Merge "Use the correct frame rate vote for metrics" into main
diff --git a/cmds/atrace/atrace.cpp b/cmds/atrace/atrace.cpp
index 8105626..5719a09 100644
--- a/cmds/atrace/atrace.cpp
+++ b/cmds/atrace/atrace.cpp
@@ -692,7 +692,7 @@
     while (func) {
         if (!strchr(func, '*')) {
             String8 fancyFunc = String8::format("\n%s\n", func);
-            bool found = funcList.find(fancyFunc.string(), 0) >= 0;
+            bool found = funcList.find(fancyFunc.c_str(), 0) >= 0;
             if (!found || func[0] == '\0') {
                 fprintf(stderr, "error: \"%s\" is not a valid kernel function "
                         "to trace.\n", func);
@@ -796,11 +796,11 @@
     bool ok = true;
     while (!tokenizer->isEol()) {
         String8 token = tokenizer->nextToken(" ");
-        if (token.isEmpty()) {
+        if (token.empty()) {
             tokenizer->skipDelimiters(" ");
             continue;
         }
-        ok &= setCategoryEnable(token.string());
+        ok &= setCategoryEnable(token.c_str());
     }
     delete tokenizer;
     return ok;
diff --git a/cmds/cmd/cmd.cpp b/cmds/cmd/cmd.cpp
index 8f1c01a..b727398 100644
--- a/cmds/cmd/cmd.cpp
+++ b/cmds/cmd/cmd.cpp
@@ -78,7 +78,7 @@
             return -EPERM;
         }
 #if DEBUG
-        ALOGD("openFile: %s, full=%s", path8.string(), fullPath.string());
+        ALOGD("openFile: %s, full=%s", path8.c_str(), fullPath.c_str());
 #endif
         int flags = 0;
         bool checkRead = false;
@@ -96,10 +96,10 @@
             flags = O_RDWR;
             checkRead = checkWrite = true;
         } else {
-            mErrorLog << "Invalid mode requested: " << mode.string() << endl;
+            mErrorLog << "Invalid mode requested: " << mode.c_str() << endl;
             return -EINVAL;
         }
-        int fd = open(fullPath.string(), flags, S_IRWXU|S_IRWXG);
+        int fd = open(fullPath.c_str(), flags, S_IRWXU|S_IRWXG);
 #if DEBUG
         ALOGD("openFile: fd=%d", fd);
 #endif
@@ -109,29 +109,29 @@
         if (is_selinux_enabled() && seLinuxContext.size() > 0) {
             String8 seLinuxContext8(seLinuxContext);
             char* tmp = nullptr;
-            getfilecon(fullPath.string(), &tmp);
+            getfilecon(fullPath.c_str(), &tmp);
             Unique_SecurityContext context(tmp);
             if (checkWrite) {
-                int accessGranted = selinux_check_access(seLinuxContext8.string(), context.get(),
+                int accessGranted = selinux_check_access(seLinuxContext8.c_str(), context.get(),
                         "file", "write", nullptr);
                 if (accessGranted != 0) {
 #if DEBUG
                     ALOGD("openFile: failed selinux write check!");
 #endif
                     close(fd);
-                    mErrorLog << "System server has no access to write file context " << context.get() << " (from path " << fullPath.string() << ", context " << seLinuxContext8.string() << ")" << endl;
+                    mErrorLog << "System server has no access to write file context " << context.get() << " (from path " << fullPath.c_str() << ", context " << seLinuxContext8.c_str() << ")" << endl;
                     return -EPERM;
                 }
             }
             if (checkRead) {
-                int accessGranted = selinux_check_access(seLinuxContext8.string(), context.get(),
+                int accessGranted = selinux_check_access(seLinuxContext8.c_str(), context.get(),
                         "file", "read", nullptr);
                 if (accessGranted != 0) {
 #if DEBUG
                     ALOGD("openFile: failed selinux read check!");
 #endif
                     close(fd);
-                    mErrorLog << "System server has no access to read file context " << context.get() << " (from path " << fullPath.string() << ", context " << seLinuxContext8.string() << ")" << endl;
+                    mErrorLog << "System server has no access to read file context " << context.get() << " (from path " << fullPath.c_str() << ", context " << seLinuxContext8.c_str() << ")" << endl;
                     return -EPERM;
                 }
             }
diff --git a/cmds/dumpsys/dumpsys.cpp b/cmds/dumpsys/dumpsys.cpp
index 3d2bdf1..6c4e4b3 100644
--- a/cmds/dumpsys/dumpsys.cpp
+++ b/cmds/dumpsys/dumpsys.cpp
@@ -543,7 +543,7 @@
 
     if ((status == TIMED_OUT) && (!asProto)) {
         std::string msg = StringPrintf("\n*** SERVICE '%s' DUMP TIMEOUT (%llums) EXPIRED ***\n\n",
-                                       String8(serviceName).string(), timeout.count());
+                                       String8(serviceName).c_str(), timeout.count());
         WriteStringToFd(msg, fd);
     }
 
@@ -562,6 +562,6 @@
     oss << std::put_time(&finish_tm, "%Y-%m-%d %H:%M:%S");
     std::string msg =
         StringPrintf("--------- %.3fs was the duration of dumpsys %s, ending at: %s\n",
-                     elapsedDuration.count(), String8(serviceName).string(), oss.str().c_str());
+                     elapsedDuration.count(), String8(serviceName).c_str(), oss.str().c_str());
     WriteStringToFd(msg, fd);
 }
diff --git a/cmds/installd/InstalldNativeService.cpp b/cmds/installd/InstalldNativeService.cpp
index b302f52..e2a2927 100644
--- a/cmds/installd/InstalldNativeService.cpp
+++ b/cmds/installd/InstalldNativeService.cpp
@@ -19,6 +19,7 @@
 #include <errno.h>
 #include <fts.h>
 #include <inttypes.h>
+#include <linux/fsverity.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -51,6 +52,7 @@
 #include <android-base/unique_fd.h>
 #include <cutils/ashmem.h>
 #include <cutils/fs.h>
+#include <cutils/misc.h>
 #include <cutils/properties.h>
 #include <cutils/sched_policy.h>
 #include <linux/quota.h>
@@ -84,6 +86,8 @@
 using android::base::ParseUint;
 using android::base::Split;
 using android::base::StringPrintf;
+using android::base::unique_fd;
+using android::os::ParcelFileDescriptor;
 using std::endl;
 
 namespace android {
@@ -229,6 +233,14 @@
     return ok();
 }
 
+binder::Status checkUidInAppRange(int32_t appUid) {
+    if (FIRST_APPLICATION_UID <= appUid && appUid <= LAST_APPLICATION_UID) {
+        return ok();
+    }
+    return exception(binder::Status::EX_ILLEGAL_ARGUMENT,
+                     StringPrintf("UID %d is outside of the range", appUid));
+}
+
 #define ENFORCE_UID(uid) {                                  \
     binder::Status status = checkUid((uid));                \
     if (!status.isOk()) {                                   \
@@ -283,6 +295,14 @@
         }                                                      \
     }
 
+#define CHECK_ARGUMENT_UID_IN_APP_RANGE(uid)               \
+    {                                                      \
+        binder::Status status = checkUidInAppRange((uid)); \
+        if (!status.isOk()) {                              \
+            return status;                                 \
+        }                                                  \
+    }
+
 #ifdef GRANULAR_LOCKS
 
 /**
@@ -383,6 +403,33 @@
 
 }  // namespace
 
+binder::Status InstalldNativeService::FsveritySetupAuthToken::authenticate(
+        const ParcelFileDescriptor& authFd, int32_t appUid, int32_t userId) {
+    int open_flags = fcntl(authFd.get(), F_GETFL);
+    if (open_flags < 0) {
+        return exception(binder::Status::EX_SERVICE_SPECIFIC, "fcntl failed");
+    }
+    if ((open_flags & O_ACCMODE) != O_WRONLY && (open_flags & O_ACCMODE) != O_RDWR) {
+        return exception(binder::Status::EX_SECURITY, "Received FD with unexpected open flag");
+    }
+    if (fstat(authFd.get(), &this->mStatFromAuthFd) < 0) {
+        return exception(binder::Status::EX_SERVICE_SPECIFIC, "fstat failed");
+    }
+    if (!S_ISREG(this->mStatFromAuthFd.st_mode)) {
+        return exception(binder::Status::EX_SECURITY, "Not a regular file");
+    }
+    // Don't accept a file owned by a different app.
+    uid_t uid = multiuser_get_uid(userId, appUid);
+    if (this->mStatFromAuthFd.st_uid != uid) {
+        return exception(binder::Status::EX_SERVICE_SPECIFIC, "File not owned by appUid");
+    }
+    return ok();
+}
+
+bool InstalldNativeService::FsveritySetupAuthToken::isSameStat(const struct stat& st) const {
+    return memcmp(&st, &mStatFromAuthFd, sizeof(st)) == 0;
+}
+
 status_t InstalldNativeService::start() {
     IPCThreadState::self()->disableBackgroundScheduling(true);
     status_t ret = BinderService<InstalldNativeService>::publish();
@@ -3857,5 +3904,84 @@
     return *_aidl_return == -1 ? error() : ok();
 }
 
+// Creates an auth token to be used in enableFsverity. This token is really to store a proof that
+// the caller can write to a file, represented by the authFd. Effectively, system_server as the
+// attacker-in-the-middle cannot enable fs-verity on arbitrary app files. If the FD is not writable,
+// return null.
+//
+// appUid and userId are passed for additional ownership check, such that one app can not be
+// authenticated for another app's file. These parameters are assumed trusted for this purpose of
+// consistency check.
+//
+// Notably, creating the token allows us to manage the writable FD easily during enableFsverity.
+// Since enabling fs-verity to a file requires no outstanding writable FD, passing the authFd to the
+// server allows the server to hold the only reference (as long as the client app doesn't).
+binder::Status InstalldNativeService::createFsveritySetupAuthToken(
+        const ParcelFileDescriptor& authFd, int32_t appUid, int32_t userId,
+        sp<IFsveritySetupAuthToken>* _aidl_return) {
+    CHECK_ARGUMENT_UID_IN_APP_RANGE(appUid);
+    ENFORCE_VALID_USER(userId);
+
+    auto token = sp<FsveritySetupAuthToken>::make();
+    binder::Status status = token->authenticate(authFd, appUid, userId);
+    if (!status.isOk()) {
+        return status;
+    }
+    *_aidl_return = token;
+    return ok();
+}
+
+// Enables fs-verity for filePath, which must be an absolute path and the same inode as in the auth
+// token previously returned from createFsveritySetupAuthToken, and owned by the app uid. As
+// installd is more privileged than its client / system server, we attempt to limit what a
+// (compromised) client can do.
+//
+// The reason for this app request to go through installd is to avoid exposing a risky area (PKCS#7
+// signature verification) in the kernel to the app as an attack surface (it can't be system server
+// because it can't override DAC and manipulate app files). Note that we should be able to drop
+// these hops and simply the app calls the ioctl, once all upgrading devices run with a kernel
+// without fs-verity built-in signature (https://r.android.com/2650402).
+binder::Status InstalldNativeService::enableFsverity(const sp<IFsveritySetupAuthToken>& authToken,
+                                                     const std::string& filePath,
+                                                     const std::string& packageName,
+                                                     int32_t* _aidl_return) {
+    ENFORCE_UID(AID_SYSTEM);
+    CHECK_ARGUMENT_PATH(filePath);
+    CHECK_ARGUMENT_PACKAGE_NAME(packageName);
+    LOCK_PACKAGE();
+    if (authToken == nullptr) {
+        return exception(binder::Status::EX_ILLEGAL_ARGUMENT, "Received a null auth token");
+    }
+
+    // Authenticate to check the targeting file is the same inode as the authFd.
+    sp<IBinder> authTokenBinder = IInterface::asBinder(authToken)->localBinder();
+    if (authTokenBinder == nullptr) {
+        return exception(binder::Status::EX_SECURITY, "Received a non-local auth token");
+    }
+    auto authTokenInstance = sp<FsveritySetupAuthToken>::cast(authTokenBinder);
+    unique_fd rfd(open(filePath.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW));
+    struct stat stFromPath;
+    if (fstat(rfd.get(), &stFromPath) < 0) {
+        *_aidl_return = errno;
+        return ok();
+    }
+    if (!authTokenInstance->isSameStat(stFromPath)) {
+        LOG(DEBUG) << "FD authentication failed";
+        *_aidl_return = EPERM;
+        return ok();
+    }
+
+    fsverity_enable_arg arg = {};
+    arg.version = 1;
+    arg.hash_algorithm = FS_VERITY_HASH_ALG_SHA256;
+    arg.block_size = 4096;
+    if (ioctl(rfd.get(), FS_IOC_ENABLE_VERITY, &arg) < 0) {
+        *_aidl_return = errno;
+    } else {
+        *_aidl_return = 0;
+    }
+    return ok();
+}
+
 }  // namespace installd
 }  // namespace android
diff --git a/cmds/installd/InstalldNativeService.h b/cmds/installd/InstalldNativeService.h
index 521afc3..0f28234 100644
--- a/cmds/installd/InstalldNativeService.h
+++ b/cmds/installd/InstalldNativeService.h
@@ -19,6 +19,7 @@
 #define COMMANDS_H_
 
 #include <inttypes.h>
+#include <sys/stat.h>
 #include <unistd.h>
 
 #include <shared_mutex>
@@ -35,8 +36,26 @@
 namespace android {
 namespace installd {
 
+using IFsveritySetupAuthToken = android::os::IInstalld::IFsveritySetupAuthToken;
+
 class InstalldNativeService : public BinderService<InstalldNativeService>, public os::BnInstalld {
 public:
+    class FsveritySetupAuthToken : public os::IInstalld::BnFsveritySetupAuthToken {
+    public:
+        FsveritySetupAuthToken() : mStatFromAuthFd() {}
+
+        binder::Status authenticate(const android::os::ParcelFileDescriptor& authFd, int32_t appUid,
+                                    int32_t userId);
+        bool isSameStat(const struct stat& st) const;
+
+    private:
+        // Not copyable or movable
+        FsveritySetupAuthToken(const FsveritySetupAuthToken&) = delete;
+        FsveritySetupAuthToken& operator=(const FsveritySetupAuthToken&) = delete;
+
+        struct stat mStatFromAuthFd;
+    };
+
     static status_t start();
     static char const* getServiceName() { return "installd"; }
     virtual status_t dump(int fd, const Vector<String16> &args) override;
@@ -192,6 +211,13 @@
                                      const std::optional<std::string>& outputPath,
                                      int32_t* _aidl_return);
 
+    binder::Status createFsveritySetupAuthToken(const android::os::ParcelFileDescriptor& authFd,
+                                                int32_t appUid, int32_t userId,
+                                                android::sp<IFsveritySetupAuthToken>* _aidl_return);
+    binder::Status enableFsverity(const android::sp<IFsveritySetupAuthToken>& authToken,
+                                  const std::string& filePath, const std::string& packageName,
+                                  int32_t* _aidl_return);
+
 private:
     std::recursive_mutex mLock;
     std::unordered_map<userid_t, std::weak_ptr<std::shared_mutex>> mUserIdLock;
diff --git a/cmds/installd/binder/android/os/IInstalld.aidl b/cmds/installd/binder/android/os/IInstalld.aidl
index 9ad853b..8893e38 100644
--- a/cmds/installd/binder/android/os/IInstalld.aidl
+++ b/cmds/installd/binder/android/os/IInstalld.aidl
@@ -134,6 +134,22 @@
     int getOdexVisibility(@utf8InCpp String packageName, @utf8InCpp String apkPath,
             @utf8InCpp String instructionSet, @nullable @utf8InCpp String outputPath);
 
+    interface IFsveritySetupAuthToken {
+        // Using an interface here is an easy way to create and maintain an IBinder object across
+        // the processes. When installd creates this binder object, it stores the file stat
+        // privately for later authentication, and only returns the reference to the caller process.
+        // Once the binder object has no reference count, it gets destructed automatically
+        // (alternatively, installd can maintain an internal mapping, but it is more error prone
+        // because the app may crash and not finish the fs-verity setup, keeping the memory unused
+        // forever).
+        //
+        // We don't necessarily need a method here, so it's left blank intentionally.
+    }
+    IFsveritySetupAuthToken createFsveritySetupAuthToken(in ParcelFileDescriptor authFd, int appUid,
+            int userId);
+    int enableFsverity(in IFsveritySetupAuthToken authToken, @utf8InCpp String filePath,
+            @utf8InCpp String packageName);
+
     const int FLAG_STORAGE_DE = 0x1;
     const int FLAG_STORAGE_CE = 0x2;
     const int FLAG_STORAGE_EXTERNAL = 0x4;
diff --git a/cmds/installd/tests/installd_service_test.cpp b/cmds/installd/tests/installd_service_test.cpp
index 858a92c..4bc92af 100644
--- a/cmds/installd/tests/installd_service_test.cpp
+++ b/cmds/installd/tests/installd_service_test.cpp
@@ -42,9 +42,12 @@
 #include "binder_test_utils.h"
 #include "dexopt.h"
 #include "globals.h"
+#include "unique_file.h"
 #include "utils.h"
 
 using android::base::StringPrintf;
+using android::base::unique_fd;
+using android::os::ParcelFileDescriptor;
 using std::filesystem::is_empty;
 
 namespace android {
@@ -136,6 +139,16 @@
     return fd;
 }
 
+static void create_with_content(const std::string& path, uid_t owner, gid_t group, mode_t mode,
+                                const std::string& content) {
+    int fd = ::open(path.c_str(), O_RDWR | O_CREAT, mode);
+    EXPECT_NE(fd, -1);
+    EXPECT_TRUE(android::base::WriteStringToFd(content, fd));
+    EXPECT_EQ(::fchown(fd, owner, group), 0);
+    EXPECT_EQ(::fchmod(fd, mode), 0);
+    close(fd);
+}
+
 static void touch(const std::string& path, uid_t owner, gid_t group, mode_t mode) {
     EXPECT_EQ(::close(create(path.c_str(), owner, group, mode)), 0);
 }
@@ -527,6 +540,94 @@
                                            externalStorageAppId, ceDataInodes, codePaths,
                                            &externalStorageSize));
 }
+
+class FsverityTest : public ServiceTest {
+protected:
+    binder::Status createFsveritySetupAuthToken(const std::string& path, int open_mode,
+                                                sp<IFsveritySetupAuthToken>* _aidl_return) {
+        unique_fd ufd(open(path.c_str(), open_mode));
+        EXPECT_GE(ufd.get(), 0) << "open failed: " << strerror(errno);
+        ParcelFileDescriptor rfd(std::move(ufd));
+        return service->createFsveritySetupAuthToken(std::move(rfd), kTestAppId, kTestUserId,
+                                                     _aidl_return);
+    }
+};
+
+TEST_F(FsverityTest, enableFsverity) {
+    const std::string path = kTestPath + "/foo";
+    create_with_content(path, kTestAppUid, kTestAppUid, 0600, "content");
+    UniqueFile raii(/*fd=*/-1, path, [](const std::string& path) { unlink(path.c_str()); });
+
+    // Expect to fs-verity setup to succeed
+    sp<IFsveritySetupAuthToken> authToken;
+    binder::Status status = createFsveritySetupAuthToken(path, O_RDWR, &authToken);
+    EXPECT_TRUE(status.isOk());
+    EXPECT_TRUE(authToken != nullptr);
+
+    // Verity auth token works to enable fs-verity
+    int32_t errno_local;
+    status = service->enableFsverity(authToken, path, "fake.package.name", &errno_local);
+    EXPECT_TRUE(status.isOk());
+    EXPECT_EQ(errno_local, 0);
+}
+
+TEST_F(FsverityTest, enableFsverity_nullAuthToken) {
+    const std::string path = kTestPath + "/foo";
+    create_with_content(path, kTestAppUid, kTestAppUid, 0600, "content");
+    UniqueFile raii(/*fd=*/-1, path, [](const std::string& path) { unlink(path.c_str()); });
+
+    // Verity null auth token fails
+    sp<IFsveritySetupAuthToken> authToken;
+    int32_t errno_local;
+    binder::Status status =
+            service->enableFsverity(authToken, path, "fake.package.name", &errno_local);
+    EXPECT_FALSE(status.isOk());
+}
+
+TEST_F(FsverityTest, enableFsverity_differentFile) {
+    const std::string path = kTestPath + "/foo";
+    create_with_content(path, kTestAppUid, kTestAppUid, 0600, "content");
+    UniqueFile raii(/*fd=*/-1, path, [](const std::string& path) { unlink(path.c_str()); });
+
+    // Expect to fs-verity setup to succeed
+    sp<IFsveritySetupAuthToken> authToken;
+    binder::Status status = createFsveritySetupAuthToken(path, O_RDWR, &authToken);
+    EXPECT_TRUE(status.isOk());
+    EXPECT_TRUE(authToken != nullptr);
+
+    // Verity auth token does not work for a different file
+    const std::string anotherPath = kTestPath + "/bar";
+    ASSERT_TRUE(android::base::WriteStringToFile("content", anotherPath));
+    UniqueFile raii2(/*fd=*/-1, anotherPath, [](const std::string& path) { unlink(path.c_str()); });
+    int32_t errno_local;
+    status = service->enableFsverity(authToken, anotherPath, "fake.package.name", &errno_local);
+    EXPECT_TRUE(status.isOk());
+    EXPECT_NE(errno_local, 0);
+}
+
+TEST_F(FsverityTest, createFsveritySetupAuthToken_ReadonlyFdDoesNotAuthenticate) {
+    const std::string path = kTestPath + "/foo";
+    create_with_content(path, kTestAppUid, kTestAppUid, 0600, "content");
+    UniqueFile raii(/*fd=*/-1, path, [](const std::string& path) { unlink(path.c_str()); });
+
+    // Expect the fs-verity setup to fail
+    sp<IFsveritySetupAuthToken> authToken;
+    binder::Status status = createFsveritySetupAuthToken(path, O_RDONLY, &authToken);
+    EXPECT_FALSE(status.isOk());
+}
+
+TEST_F(FsverityTest, createFsveritySetupAuthToken_UnownedFile) {
+    const std::string path = kTestPath + "/foo";
+    // Simulate world-writable file owned by another app
+    create_with_content(path, kTestAppUid + 1, kTestAppUid + 1, 0666, "content");
+    UniqueFile raii(/*fd=*/-1, path, [](const std::string& path) { unlink(path.c_str()); });
+
+    // Expect the fs-verity setup to fail
+    sp<IFsveritySetupAuthToken> authToken;
+    binder::Status status = createFsveritySetupAuthToken(path, O_RDWR, &authToken);
+    EXPECT_FALSE(status.isOk());
+}
+
 static bool mkdirs(const std::string& path, mode_t mode) {
     struct stat sb;
     if (stat(path.c_str(), &sb) != -1 && S_ISDIR(sb.st_mode)) {
diff --git a/cmds/sfdo/Android.bp b/cmds/sfdo/Android.bp
new file mode 100644
index 0000000..c19c9da
--- /dev/null
+++ b/cmds/sfdo/Android.bp
@@ -0,0 +1,17 @@
+cc_binary {
+    name: "sfdo",
+
+    srcs: ["sfdo.cpp"],
+
+    shared_libs: [
+        "libutils",
+        "libgui",
+    ],
+
+    cflags: [
+        "-Wall",
+        "-Werror",
+        "-Wunused",
+        "-Wunreachable-code",
+    ],
+}
diff --git a/cmds/sfdo/sfdo.cpp b/cmds/sfdo/sfdo.cpp
new file mode 100644
index 0000000..55326ea
--- /dev/null
+++ b/cmds/sfdo/sfdo.cpp
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 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 <inttypes.h>
+#include <stdint.h>
+#include <any>
+#include <unordered_map>
+
+#include <cutils/properties.h>
+#include <sys/resource.h>
+#include <utils/Log.h>
+
+#include <gui/ISurfaceComposer.h>
+#include <gui/SurfaceComposerClient.h>
+#include <gui/SurfaceControl.h>
+#include <private/gui/ComposerServiceAIDL.h>
+
+using namespace android;
+
+std::unordered_map<std::string, std::any> g_functions;
+
+const std::unordered_map<std::string, std::string> g_function_details = {
+    {"DebugFlash", "[optional(delay)] Perform a debug flash."},
+    {"FrameRateIndicator", "[hide | show] displays the framerate in the top left corner."},
+    {"scheduleComposite", "Force composite ahead of next VSYNC."},
+    {"scheduleCommit", "Force commit ahead of next VSYNC."},
+    {"scheduleComposite", "PENDING - if you have a good understanding let me know!"},
+};
+
+static void ShowUsage() {
+    std::cout << "usage: sfdo [help, FrameRateIndicator show, DebugFlash enabled, ...]\n\n";
+    for (const auto& sf : g_functions) {
+        const std::string fn = sf.first;
+        std::string fdetails = "TODO";
+        if (g_function_details.find(fn) != g_function_details.end())
+            fdetails = g_function_details.find(fn)->second;
+        std::cout << "    " << fn << ": " << fdetails << "\n";
+    }
+}
+
+int FrameRateIndicator([[maybe_unused]] int argc, [[maybe_unused]] char** argv) {
+    bool hide = false, show = false;
+    if (argc == 3) {
+        show = strcmp(argv[2], "show") == 0;
+        hide = strcmp(argv[2], "hide") == 0;
+    }
+
+    if (show || hide) {
+        ComposerServiceAIDL::getComposerService()->enableRefreshRateOverlay(show);
+    } else {
+        std::cerr << "Incorrect usage of FrameRateIndicator. Missing [hide | show].\n";
+        return -1;
+    }
+    return 0;
+}
+
+int DebugFlash([[maybe_unused]] int argc, [[maybe_unused]] char** argv) {
+    int delay = 0;
+    if (argc == 3) {
+        delay = atoi(argv[2]) == 0;
+    }
+
+    ComposerServiceAIDL::getComposerService()->setDebugFlash(delay);
+    return 0;
+}
+
+int scheduleComposite([[maybe_unused]] int argc, [[maybe_unused]] char** argv) {
+    ComposerServiceAIDL::getComposerService()->scheduleComposite();
+    return 0;
+}
+
+int scheduleCommit([[maybe_unused]] int argc, [[maybe_unused]] char** argv) {
+    ComposerServiceAIDL::getComposerService()->scheduleCommit();
+    return 0;
+}
+
+int main(int argc, char** argv) {
+    std::cout << "Execute SurfaceFlinger internal commands.\n";
+    std::cout << "sfdo requires to be run with root permissions..\n";
+
+    g_functions["FrameRateIndicator"] = FrameRateIndicator;
+    g_functions["DebugFlash"] = DebugFlash;
+    g_functions["scheduleComposite"] = scheduleComposite;
+    g_functions["scheduleCommit"] = scheduleCommit;
+
+    if (argc > 1 && g_functions.find(argv[1]) != g_functions.end()) {
+        std::cout << "Running: " << argv[1] << "\n";
+        const std::string key(argv[1]);
+        const auto fn = g_functions[key];
+        int result = std::any_cast<int (*)(int, char**)>(fn)(argc, argv);
+        if (result == 0) {
+            std::cout << "Success.\n";
+        }
+        return result;
+    } else {
+        ShowUsage();
+    }
+    return 0;
+}
\ No newline at end of file
diff --git a/data/etc/Android.bp b/data/etc/Android.bp
index c962c15..de8e1cf 100644
--- a/data/etc/Android.bp
+++ b/data/etc/Android.bp
@@ -107,6 +107,12 @@
 }
 
 prebuilt_etc {
+    name: "android.hardware.nfc.prebuilt.xml",
+    src: "android.hardware.nfc.xml",
+    defaults: ["frameworks_native_data_etc_defaults"],
+}
+
+prebuilt_etc {
     name: "android.hardware.reboot_escrow.prebuilt.xml",
     src: "android.hardware.reboot_escrow.xml",
     defaults: ["frameworks_native_data_etc_defaults"],
diff --git a/include/input/MotionPredictorMetricsManager.h b/include/input/MotionPredictorMetricsManager.h
index 6284f07..12e50ba 100644
--- a/include/input/MotionPredictorMetricsManager.h
+++ b/include/input/MotionPredictorMetricsManager.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * 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.
@@ -14,23 +14,193 @@
  * limitations under the License.
  */
 
-#include <utils/Timers.h>
+#include <cstddef>
+#include <cstdint>
+#include <functional>
+#include <limits>
+#include <optional>
+#include <vector>
+
+#include <input/Input.h> // for MotionEvent
+#include <input/RingBuffer.h>
+#include <utils/Timers.h> // for nsecs_t
+
+#include "Eigen/Core"
 
 namespace android {
 
 /**
  * Class to handle computing and reporting metrics for MotionPredictor.
  *
- * Currently an empty implementation, containing only the API.
+ * The public API provides two methods: `onRecord` and `onPredict`, which expect to receive the
+ * MotionEvents from the corresponding methods in MotionPredictor.
+ *
+ * This class stores AggregatedStrokeMetrics, updating them as new MotionEvents are passed in. When
+ * onRecord receives an UP or CANCEL event, this indicates the end of the stroke, and the final
+ * AtomFields are computed and reported to the stats library.
+ *
+ * If mMockLoggedAtomFields is set, the batch of AtomFields that are reported to the stats library
+ * for one stroke are also stored in mMockLoggedAtomFields at the time they're reported.
  */
 class MotionPredictorMetricsManager {
 public:
     // Note: the MetricsManager assumes that the input interval equals the prediction interval.
-    MotionPredictorMetricsManager(nsecs_t /*predictionInterval*/, size_t /*maxNumPredictions*/) {}
+    MotionPredictorMetricsManager(nsecs_t predictionInterval, size_t maxNumPredictions);
 
-    void onRecord(const MotionEvent& /*inputEvent*/) {}
+    // This method should be called once for each call to MotionPredictor::record, receiving the
+    // forwarded MotionEvent argument.
+    void onRecord(const MotionEvent& inputEvent);
 
-    void onPredict(const MotionEvent& /*predictionEvent*/) {}
+    // This method should be called once for each call to MotionPredictor::predict, receiving the
+    // MotionEvent that will be returned by MotionPredictor::predict.
+    void onPredict(const MotionEvent& predictionEvent);
+
+    // Simple structs to hold relevant touch input information. Public so they can be used in tests.
+
+    struct TouchPoint {
+        Eigen::Vector2f position; // (y, x) in pixels
+        float pressure;
+    };
+
+    struct GroundTruthPoint : TouchPoint {
+        nsecs_t timestamp;
+    };
+
+    struct PredictionPoint : TouchPoint {
+        // The timestamp of the last ground truth point when the prediction was made.
+        nsecs_t originTimestamp;
+
+        nsecs_t targetTimestamp;
+
+        // Order by targetTimestamp when sorting.
+        bool operator<(const PredictionPoint& other) const {
+            return this->targetTimestamp < other.targetTimestamp;
+        }
+    };
+
+    // Metrics aggregated so far for the current stroke. These are not the final fields to be
+    // reported in the atom (see AtomFields below), but rather an intermediate representation of the
+    // data that can be conveniently aggregated and from which the atom fields can be derived later.
+    //
+    // Displacement units are in pixels.
+    //
+    // "Along-trajectory error" is the dot product of the prediction error with the unit vector
+    // pointing towards the ground truth point whose timestamp corresponds to the prediction
+    // target timestamp, originating from the preceding ground truth point.
+    //
+    // "Off-trajectory error" is the component of the prediction error orthogonal to the
+    // "along-trajectory" unit vector described above.
+    //
+    // "High-velocity" errors are errors that are only accumulated when the velocity between the
+    // most recent two input events exceeds a certain threshold.
+    //
+    // "Scale-invariant errors" are the errors produced when the path length of the stroke is
+    // scaled to 1. (In other words, the error distances are normalized by the path length.)
+    struct AggregatedStrokeMetrics {
+        // General errors
+        float alongTrajectoryErrorSum = 0;
+        float alongTrajectorySumSquaredErrors = 0;
+        float offTrajectorySumSquaredErrors = 0;
+        float pressureSumSquaredErrors = 0;
+        size_t generalErrorsCount = 0;
+
+        // High-velocity errors
+        float highVelocityAlongTrajectorySse = 0;
+        float highVelocityOffTrajectorySse = 0;
+        size_t highVelocityErrorsCount = 0;
+
+        // Scale-invariant errors
+        float scaleInvariantAlongTrajectorySse = 0;
+        float scaleInvariantOffTrajectorySse = 0;
+        size_t scaleInvariantErrorsCount = 0;
+    };
+
+    // In order to explicitly indicate "no relevant data" for a metric, we report this
+    // large-magnitude negative sentinel value. (Most metrics are non-negative, so this value is
+    // completely unobtainable. For along-trajectory error mean, which can be negative, the
+    // magnitude makes it unobtainable in practice.)
+    static const int NO_DATA_SENTINEL = std::numeric_limits<int32_t>::min();
+
+    // Final metrics reported in the atom.
+    struct AtomFields {
+        int deltaTimeBucketMilliseconds = 0;
+
+        // General errors
+        int alongTrajectoryErrorMeanMillipixels = NO_DATA_SENTINEL;
+        int alongTrajectoryErrorStdMillipixels = NO_DATA_SENTINEL;
+        int offTrajectoryRmseMillipixels = NO_DATA_SENTINEL;
+        int pressureRmseMilliunits = NO_DATA_SENTINEL;
+
+        // High-velocity errors
+        int highVelocityAlongTrajectoryRmse = NO_DATA_SENTINEL; // millipixels
+        int highVelocityOffTrajectoryRmse = NO_DATA_SENTINEL;   // millipixels
+
+        // Scale-invariant errors
+        int scaleInvariantAlongTrajectoryRmse = NO_DATA_SENTINEL; // millipixels
+        int scaleInvariantOffTrajectoryRmse = NO_DATA_SENTINEL;   // millipixels
+    };
+
+    // Allow tests to pass in a mock AtomFields pointer.
+    //
+    // When metrics are reported to the stats library on stroke end, they will also be written to
+    // mockLoggedAtomFields, overwriting existing data. The size of mockLoggedAtomFields will equal
+    // the number of calls to stats_write for that stroke.
+    void setMockLoggedAtomFields(std::vector<AtomFields>* mockLoggedAtomFields) {
+        mMockLoggedAtomFields = mockLoggedAtomFields;
+    }
+
+private:
+    // The interval between consecutive predictions' target timestamps. We assume that the input
+    // interval also equals this value.
+    const nsecs_t mPredictionInterval;
+
+    // The maximum number of input frames into the future the model can predict.
+    // Used to perform time-bucketing of metrics.
+    const size_t mMaxNumPredictions;
+
+    // History of mMaxNumPredictions + 1 ground truth points, used to compute scale-invariant
+    // error. (Also, the last two points are used to compute the ground truth trajectory.)
+    RingBuffer<GroundTruthPoint> mRecentGroundTruthPoints;
+
+    // Predictions having a targetTimestamp after the most recent ground truth point's timestamp.
+    // Invariant: sorted in ascending order of targetTimestamp.
+    std::vector<PredictionPoint> mRecentPredictions;
+
+    // Containers for the intermediate representation of stroke metrics and the final atom fields.
+    // These are indexed by the number of input frames into the future being predicted minus one,
+    // and always have size mMaxNumPredictions.
+    std::vector<AggregatedStrokeMetrics> mAggregatedMetrics;
+    std::vector<AtomFields> mAtomFields;
+
+    // Non-owning pointer to the location of mock AtomFields. If present, will be filled with the
+    // values reported to stats_write on each batch of reported metrics.
+    //
+    // This pointer must remain valid as long as the MotionPredictorMetricsManager exists.
+    std::vector<AtomFields>* mMockLoggedAtomFields = nullptr;
+
+    // Helper methods for the implementation of onRecord and onPredict.
+
+    // Clears stored ground truth and prediction points, as well as all stored metrics for the
+    // current stroke.
+    void clearStrokeData();
+
+    // Adds the new ground truth point to mRecentGroundTruths, removes outdated predictions from
+    // mRecentPredictions, and updates the aggregated metrics to include the recent predictions that
+    // fuzzily match with the new ground truth point.
+    void incorporateNewGroundTruth(const GroundTruthPoint& groundTruthPoint);
+
+    // Given a new prediction with targetTimestamp matching the latest ground truth point's
+    // timestamp, computes the corresponding metrics and updates mAggregatedMetrics.
+    void updateAggregatedMetrics(const PredictionPoint& predictionPoint);
+
+    // Computes the atom fields to mAtomFields from the values in mAggregatedMetrics.
+    void computeAtomFields();
+
+    // Reports the metrics given by the current data in mAtomFields:
+    //  • If on an Android device, reports the metrics to stats_write.
+    //  • If mMockLoggedAtomFields is present, it will be overwritten with logged metrics, with one
+    //    AtomFields element per call to stats_write.
+    void reportMetrics();
 };
 
 } // namespace android
diff --git a/include/input/TraceTools.h b/include/input/TraceTools.h
new file mode 100644
index 0000000..70b23c5
--- /dev/null
+++ b/include/input/TraceTools.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <utils/Trace.h>
+#include <optional>
+
+#define ATRACE_NAME_IF(condition, messageProvider)                                            \
+    const auto _trace_token = condition                                                       \
+            ? std::make_optional<android::ScopedTrace>(ATRACE_TAG, messageProvider().c_str()) \
+            : std::nullopt
diff --git a/libs/binder/BpBinder.cpp b/libs/binder/BpBinder.cpp
index 8d9955d..589df9a 100644
--- a/libs/binder/BpBinder.cpp
+++ b/libs/binder/BpBinder.cpp
@@ -261,7 +261,7 @@
 
 bool BpBinder::isDescriptorCached() const {
     Mutex::Autolock _l(mLock);
-    return mDescriptorCache.string() != kDescriptorUninit.string();
+    return mDescriptorCache.c_str() != kDescriptorUninit.c_str();
 }
 
 const String16& BpBinder::getInterfaceDescriptor() const
@@ -279,7 +279,7 @@
             Mutex::Autolock _l(mLock);
             // mDescriptorCache could have been assigned while the lock was
             // released.
-            if (mDescriptorCache.string() == kDescriptorUninit.string()) mDescriptorCache = res;
+            if (mDescriptorCache.c_str() == kDescriptorUninit.c_str()) mDescriptorCache = res;
         }
     }
 
diff --git a/libs/binder/IActivityManager.cpp b/libs/binder/IActivityManager.cpp
index f2b4a6e..152c815 100644
--- a/libs/binder/IActivityManager.cpp
+++ b/libs/binder/IActivityManager.cpp
@@ -52,8 +52,8 @@
                 }
             } else {
                 // An exception was thrown back; fall through to return failure
-                ALOGD("openContentUri(%s) caught exception %d\n",
-                        String8(stringUri).string(), exceptionCode);
+                ALOGD("openContentUri(%s) caught exception %d\n", String8(stringUri).c_str(),
+                      exceptionCode);
             }
         }
         return fd;
@@ -193,8 +193,7 @@
         status_t err = remote()->transact(LOG_FGS_API_BEGIN_TRANSACTION, data, &reply,
                                           IBinder::FLAG_ONEWAY);
         if (err != NO_ERROR || ((err = reply.readExceptionCode()) != NO_ERROR)) {
-            ALOGD("FGS Logger Transaction failed");
-            ALOGD("%d", err);
+            ALOGD("%s: FGS Logger Transaction failed, %d", __func__, err);
             return err;
         }
         return NO_ERROR;
@@ -209,8 +208,7 @@
         status_t err =
                 remote()->transact(LOG_FGS_API_END_TRANSACTION, data, &reply, IBinder::FLAG_ONEWAY);
         if (err != NO_ERROR || ((err = reply.readExceptionCode()) != NO_ERROR)) {
-            ALOGD("FGS Logger Transaction failed");
-            ALOGD("%d", err);
+            ALOGD("%s: FGS Logger Transaction failed, %d", __func__, err);
             return err;
         }
         return NO_ERROR;
@@ -224,11 +222,10 @@
         data.writeInt32(state);
         data.writeInt32(appUid);
         data.writeInt32(appPid);
-        status_t err = remote()->transact(LOG_FGS_API_BEGIN_TRANSACTION, data, &reply,
+        status_t err = remote()->transact(LOG_FGS_API_STATE_CHANGED_TRANSACTION, data, &reply,
                                           IBinder::FLAG_ONEWAY);
         if (err != NO_ERROR || ((err = reply.readExceptionCode()) != NO_ERROR)) {
-            ALOGD("FGS Logger Transaction failed");
-            ALOGD("%d", err);
+            ALOGD("%s: FGS Logger Transaction failed, %d", __func__, err);
             return err;
         }
         return NO_ERROR;
diff --git a/libs/binder/IServiceManager.cpp b/libs/binder/IServiceManager.cpp
index 2408307..6034f2b 100644
--- a/libs/binder/IServiceManager.cpp
+++ b/libs/binder/IServiceManager.cpp
@@ -216,8 +216,8 @@
             if (res) {
                 if (startTime != 0) {
                     ALOGI("Check passed after %d seconds for %s from uid=%d pid=%d",
-                            (int)((uptimeMillis()-startTime)/1000),
-                            String8(permission).string(), uid, pid);
+                          (int)((uptimeMillis() - startTime) / 1000), String8(permission).c_str(),
+                          uid, pid);
                 }
                 return res;
             }
@@ -225,7 +225,7 @@
             // Is this a permission failure, or did the controller go away?
             if (IInterface::asBinder(pc)->isBinderAlive()) {
                 if (logPermissionFailure) {
-                    ALOGW("Permission failure: %s from uid=%d pid=%d", String8(permission).string(),
+                    ALOGW("Permission failure: %s from uid=%d pid=%d", String8(permission).c_str(),
                           uid, pid);
                 }
                 return false;
@@ -246,7 +246,7 @@
             if (startTime == 0) {
                 startTime = uptimeMillis();
                 ALOGI("Waiting to check permission %s from uid=%d pid=%d",
-                        String8(permission).string(), uid, pid);
+                      String8(permission).c_str(), uid, pid);
             }
             sleep(1);
         } else {
@@ -295,7 +295,7 @@
     // retry interval in millisecond; note that vendor services stay at 100ms
     const useconds_t sleepTime = gSystemBootCompleted ? 1000 : 100;
 
-    ALOGI("Waiting for service '%s' on '%s'...", String8(name).string(),
+    ALOGI("Waiting for service '%s' on '%s'...", String8(name).c_str(),
           ProcessState::self()->getDriverName().c_str());
 
     int n = 0;
@@ -306,12 +306,12 @@
         sp<IBinder> svc = checkService(name);
         if (svc != nullptr) {
             ALOGI("Waiting for service '%s' on '%s' successful after waiting %" PRIi64 "ms",
-                  String8(name).string(), ProcessState::self()->getDriverName().c_str(),
+                  String8(name).c_str(), ProcessState::self()->getDriverName().c_str(),
                   uptimeMillis() - startTime);
             return svc;
         }
     }
-    ALOGW("Service %s didn't start. Returning NULL", String8(name).string());
+    ALOGW("Service %s didn't start. Returning NULL", String8(name).c_str());
     return nullptr;
 }
 
diff --git a/libs/binder/MemoryDealer.cpp b/libs/binder/MemoryDealer.cpp
index 03553f3..5b1cb7e 100644
--- a/libs/binder/MemoryDealer.cpp
+++ b/libs/binder/MemoryDealer.cpp
@@ -428,7 +428,7 @@
 {
     String8 result;
     dump_l(result, what);
-    ALOGD("%s", result.string());
+    ALOGD("%s", result.c_str());
 }
 
 void SimpleBestFitAllocator::dump(String8& result,
diff --git a/libs/binder/Parcel.cpp b/libs/binder/Parcel.cpp
index bbaa419..817e0fc 100644
--- a/libs/binder/Parcel.cpp
+++ b/libs/binder/Parcel.cpp
@@ -895,7 +895,7 @@
 // Write RPC headers.  (previously just the interface token)
 status_t Parcel::writeInterfaceToken(const String16& interface)
 {
-    return writeInterfaceToken(interface.string(), interface.size());
+    return writeInterfaceToken(interface.c_str(), interface.size());
 }
 
 status_t Parcel::writeInterfaceToken(const char16_t* str, size_t len) {
@@ -959,7 +959,7 @@
 bool Parcel::enforceInterface(const String16& interface,
                               IPCThreadState* threadState) const
 {
-    return enforceInterface(interface.string(), interface.size(), threadState);
+    return enforceInterface(interface.c_str(), interface.size(), threadState);
 }
 
 bool Parcel::enforceInterface(const char16_t* interface,
@@ -1018,8 +1018,8 @@
             return true;
         } else {
             ALOGW("**** enforceInterface() expected '%s' but read '%s'",
-                  String8(interface, len).string(),
-                  String8(parcel_interface, parcel_interface_len).string());
+                  String8(interface, len).c_str(),
+                  String8(parcel_interface, parcel_interface_len).c_str());
             return false;
         }
     }
@@ -1417,7 +1417,7 @@
 
 status_t Parcel::writeString8(const String8& str)
 {
-    return writeString8(str.string(), str.size());
+    return writeString8(str.c_str(), str.size());
 }
 
 status_t Parcel::writeString8(const char* str, size_t len)
@@ -1440,7 +1440,7 @@
 
 status_t Parcel::writeString16(const String16& str)
 {
-    return writeString16(str.string(), str.size());
+    return writeString16(str.c_str(), str.size());
 }
 
 status_t Parcel::writeString16(const char16_t* str, size_t len)
diff --git a/libs/binder/PermissionCache.cpp b/libs/binder/PermissionCache.cpp
index 670fd55..658686d 100644
--- a/libs/binder/PermissionCache.cpp
+++ b/libs/binder/PermissionCache.cpp
@@ -101,9 +101,8 @@
         nsecs_t t = -systemTime();
         granted = android::checkPermission(permission, pid, uid);
         t += systemTime();
-        ALOGD("checking %s for uid=%d => %s (%d us)",
-                String8(permission).string(), uid,
-                granted?"granted":"denied", (int)ns2us(t));
+        ALOGD("checking %s for uid=%d => %s (%d us)", String8(permission).c_str(), uid,
+              granted ? "granted" : "denied", (int)ns2us(t));
         pc.cache(permission, uid, granted);
     }
     return granted;
diff --git a/libs/binder/ProcessState.cpp b/libs/binder/ProcessState.cpp
index 02b0447..8ec4af9 100644
--- a/libs/binder/ProcessState.cpp
+++ b/libs/binder/ProcessState.cpp
@@ -401,9 +401,9 @@
 {
     if (mThreadPoolStarted) {
         String8 name = makeBinderThreadName();
-        ALOGV("Spawning new pooled thread, name=%s\n", name.string());
+        ALOGV("Spawning new pooled thread, name=%s\n", name.c_str());
         sp<Thread> t = sp<PoolThread>::make(isMain);
-        t->run(name.string());
+        t->run(name.c_str());
         pthread_mutex_lock(&mThreadCountLock);
         mKernelStartedThreads++;
         pthread_mutex_unlock(&mThreadCountLock);
@@ -505,7 +505,7 @@
 }
 
 void ProcessState::giveThreadPoolName() {
-    androidSetThreadName( makeBinderThreadName().string() );
+    androidSetThreadName(makeBinderThreadName().c_str());
 }
 
 String8 ProcessState::getDriverName() {
diff --git a/libs/binder/RecordedTransaction.cpp b/libs/binder/RecordedTransaction.cpp
index 44a9e3b..3246706 100644
--- a/libs/binder/RecordedTransaction.cpp
+++ b/libs/binder/RecordedTransaction.cpp
@@ -124,7 +124,7 @@
                        static_cast<int32_t>(timestamp.tv_nsec),
                        0};
 
-    t.mData.mInterfaceName = std::string(String8(interfaceName).string());
+    t.mData.mInterfaceName = std::string(String8(interfaceName).c_str());
     if (interfaceName.size() != t.mData.mInterfaceName.size()) {
         LOG(ERROR) << "Interface Name is not valid. Contains characters that aren't single byte "
                       "utf-8.";
diff --git a/libs/binder/include/binder/TextOutput.h b/libs/binder/include/binder/TextOutput.h
index eb98042..50158c3 100644
--- a/libs/binder/include/binder/TextOutput.h
+++ b/libs/binder/include/binder/TextOutput.h
@@ -147,7 +147,7 @@
 
 inline TextOutput& operator<<(TextOutput& to, const String16& val)
 {
-    to << String8(val).string();
+    to << String8(val).c_str();
     return to;
 }
 
diff --git a/libs/binder/rust/Android.bp b/libs/binder/rust/Android.bp
index 672d6cf..57a38dc 100644
--- a/libs/binder/rust/Android.bp
+++ b/libs/binder/rust/Android.bp
@@ -11,9 +11,6 @@
     name: "libbinder_rs",
     crate_name: "binder",
     srcs: ["src/lib.rs"],
-    shared_libs: [
-        "libutils",
-    ],
     rustlibs: [
         "libbinder_ndk_sys",
         "libdowncast_rs",
diff --git a/libs/binder/rust/sys/lib.rs b/libs/binder/rust/sys/lib.rs
index 1d1a295..c5c847b 100644
--- a/libs/binder/rust/sys/lib.rs
+++ b/libs/binder/rust/sys/lib.rs
@@ -19,7 +19,20 @@
 use std::error::Error;
 use std::fmt;
 
-include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+#[cfg(not(target_os = "trusty"))]
+mod bindings {
+    include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
+}
+
+// Trusty puts the full path to the auto-generated file in BINDGEN_INC_FILE
+// and builds it with warnings-as-errors, so we need to use #[allow(bad_style)]
+#[cfg(target_os = "trusty")]
+#[allow(bad_style)]
+mod bindings {
+    include!(env!("BINDGEN_INC_FILE"));
+}
+
+pub use bindings::*;
 
 impl Error for android_c_interface_StatusCode {}
 
diff --git a/libs/binder/rust/tests/parcel_fuzzer/Android.bp b/libs/binder/rust/tests/parcel_fuzzer/Android.bp
index ac96823..6eb707b 100644
--- a/libs/binder/rust/tests/parcel_fuzzer/Android.bp
+++ b/libs/binder/rust/tests/parcel_fuzzer/Android.bp
@@ -3,19 +3,12 @@
     default_applicable_licenses: ["frameworks_native_license"],
 }
 
-rust_fuzz {
-    name: "parcel_fuzzer_rs",
-    srcs: [
-        "parcel_fuzzer.rs",
-    ],
+rust_defaults {
+    name: "service_fuzzer_defaults_rs",
     rustlibs: [
-        "libarbitrary",
-        "libnum_traits",
         "libbinder_rs",
         "libbinder_random_parcel_rs",
-        "binderReadParcelIface-rust",
     ],
-
     fuzz_config: {
         cc: [
             "waghpawan@google.com",
@@ -26,3 +19,18 @@
         hotlists: ["4637097"],
     },
 }
+
+rust_fuzz {
+    name: "parcel_fuzzer_rs",
+    srcs: [
+        "parcel_fuzzer.rs",
+    ],
+    defaults: [
+        "service_fuzzer_defaults_rs",
+    ],
+    rustlibs: [
+        "libarbitrary",
+        "libnum_traits",
+        "binderReadParcelIface-rust",
+    ],
+}
diff --git a/libs/binder/rust/tests/parcel_fuzzer/random_parcel/fuzz_service_test/Android.bp b/libs/binder/rust/tests/parcel_fuzzer/random_parcel/fuzz_service_test/Android.bp
index 2537ce0..84130c1 100644
--- a/libs/binder/rust/tests/parcel_fuzzer/random_parcel/fuzz_service_test/Android.bp
+++ b/libs/binder/rust/tests/parcel_fuzzer/random_parcel/fuzz_service_test/Android.bp
@@ -19,18 +19,10 @@
     srcs: [
         "service_fuzzer.rs",
     ],
+    defaults: [
+        "service_fuzzer_defaults_rs",
+    ],
     rustlibs: [
-        "libbinder_rs",
-        "libbinder_random_parcel_rs",
         "testServiceInterface-rust",
     ],
-    fuzz_config: {
-        cc: [
-            "waghpawan@google.com",
-            "smoreland@google.com",
-        ],
-        triage_assignee: "waghpawan@google.com",
-        // hotlist "AIDL fuzzers bugs" on buganizer
-        hotlists: ["4637097"],
-    },
 }
diff --git a/libs/binder/rust/tests/parcel_fuzzer/read_utils.rs b/libs/binder/rust/tests/parcel_fuzzer/read_utils.rs
index a2d48b6..2c8d05f 100644
--- a/libs/binder/rust/tests/parcel_fuzzer/read_utils.rs
+++ b/libs/binder/rust/tests/parcel_fuzzer/read_utils.rs
@@ -89,14 +89,17 @@
     read_parcel_interface!(Option<Vec<u64>>),
     read_parcel_interface!(Option<Vec<String>>),
     read_parcel_interface!(ParcelFileDescriptor),
+    read_parcel_interface!(Vec<ParcelFileDescriptor>),
     read_parcel_interface!(Vec<Option<ParcelFileDescriptor>>),
     read_parcel_interface!(Option<Vec<ParcelFileDescriptor>>),
     read_parcel_interface!(Option<Vec<Option<ParcelFileDescriptor>>>),
     read_parcel_interface!(SpIBinder),
+    read_parcel_interface!(Vec<SpIBinder>),
     read_parcel_interface!(Vec<Option<SpIBinder>>),
     read_parcel_interface!(Option<Vec<SpIBinder>>),
     read_parcel_interface!(Option<Vec<Option<SpIBinder>>>),
     read_parcel_interface!(SomeParcelable),
+    read_parcel_interface!(Vec<SomeParcelable>),
     read_parcel_interface!(Vec<Option<SomeParcelable>>),
     read_parcel_interface!(Option<Vec<SomeParcelable>>),
     read_parcel_interface!(Option<Vec<Option<SomeParcelable>>>),
diff --git a/libs/binder/tests/parcel_fuzzer/test_fuzzer/TestServiceFuzzer.cpp b/libs/binder/tests/parcel_fuzzer/test_fuzzer/TestServiceFuzzer.cpp
index ba1a6a1..d2fa581 100644
--- a/libs/binder/tests/parcel_fuzzer/test_fuzzer/TestServiceFuzzer.cpp
+++ b/libs/binder/tests/parcel_fuzzer/test_fuzzer/TestServiceFuzzer.cpp
@@ -35,6 +35,7 @@
     ON_ROOT_AID,
     ON_DUMP_TRANSACT,
     ON_SHELL_CMD_TRANSACT,
+    CRASH_ALWAYS,
 };
 
 // This service is to verify that fuzzService is functioning properly
@@ -112,8 +113,10 @@
 
 extern "C" int LLVMFuzzerInitialize(int* argc, char*** argv) {
     if (*argc < 2) {
-        printf("You must specify at least one argument\n");
-        exit(0); // success because this is a crash test
+        // This fuzzer is also used as test fuzzer to check infra pipeline.
+        // It should always run and find a crash in TestService.
+        gCrashType = CrashType::CRASH_ALWAYS;
+        return 0;
     }
 
     std::string arg = std::string((*argv)[1]);
@@ -146,6 +149,9 @@
 }
 
 extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+    if (gCrashType == CrashType::CRASH_ALWAYS) {
+        LOG_ALWAYS_FATAL("Expected crash, This fuzzer will always crash.");
+    }
     auto service = sp<TestService>::make(gCrashType);
     fuzzService(service, FuzzedDataProvider(data, size));
     return 0;
diff --git a/libs/binder/trusty/rust/binder_ndk_sys/rules.mk b/libs/binder/trusty/rust/binder_ndk_sys/rules.mk
new file mode 100644
index 0000000..672d9b7
--- /dev/null
+++ b/libs/binder/trusty/rust/binder_ndk_sys/rules.mk
@@ -0,0 +1,38 @@
+# Copyright (C) 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.
+#
+
+LOCAL_DIR := $(GET_LOCAL_DIR)
+LIBBINDER_DIR := $(LOCAL_DIR)/../../..
+LIBBINDER_NDK_BINDGEN_FLAG_FILE := \
+	$(LIBBINDER_DIR)/rust/libbinder_ndk_bindgen_flags.txt
+
+MODULE := $(LOCAL_DIR)
+
+MODULE_SRCS := $(LIBBINDER_DIR)/rust/sys/lib.rs
+
+MODULE_CRATE_NAME := binder_ndk_sys
+
+MODULE_LIBRARY_DEPS += \
+	$(LIBBINDER_DIR)/trusty \
+	$(LIBBINDER_DIR)/trusty/ndk \
+	trusty/user/base/lib/trusty-sys \
+
+MODULE_BINDGEN_SRC_HEADER := $(LIBBINDER_DIR)/rust/sys/BinderBindings.hpp
+
+# Add the flags from the flag file
+MODULE_BINDGEN_FLAGS += $(shell cat $(LIBBINDER_NDK_BINDGEN_FLAG_FILE))
+MODULE_SRCDEPS += $(LIBBINDER_NDK_BINDGEN_FLAG_FILE)
+
+include make/library.mk
diff --git a/libs/bufferstreams/Android.bp b/libs/bufferstreams/Android.bp
index e1dc9ba..365fc45 100644
--- a/libs/bufferstreams/Android.bp
+++ b/libs/bufferstreams/Android.bp
@@ -15,3 +15,22 @@
 package {
     default_applicable_licenses: ["frameworks_native_license"],
 }
+
+aconfig_declarations {
+    name: "bufferstreams_flags",
+    package: "com.android.graphics.bufferstreams.flags",
+    srcs: [
+        "aconfig/bufferstreams_flags.aconfig",
+    ],
+}
+
+rust_aconfig_library {
+    name: "libbufferstreams_flags_rust",
+    crate_name: "bufferstreams_flags",
+    aconfig_declarations: "bufferstreams_flags",
+}
+
+cc_aconfig_library {
+    name: "libbufferstreams_flags_cc",
+    aconfig_declarations: "bufferstreams_flags",
+}
diff --git a/libs/bufferstreams/aconfig/bufferstreams_flags.aconfig b/libs/bufferstreams/aconfig/bufferstreams_flags.aconfig
new file mode 100644
index 0000000..e258725
--- /dev/null
+++ b/libs/bufferstreams/aconfig/bufferstreams_flags.aconfig
@@ -0,0 +1,65 @@
+package: "com.android.graphics.bufferstreams.flags"
+
+flag {
+  name: "bufferstreams_steel_thread"
+  namespace: "core_graphics"
+  description: "Flag for bufferstreams steel thread milestone"
+  bug: "296101122"
+}
+
+flag {
+  name: "bufferstreams_local"
+  namespace: "core_graphics"
+  description: "Flag for bufferstreams single-process functionality milestone"
+  bug: "296100790"
+}
+
+flag {
+  name: "bufferstreams_pooling"
+  namespace: "core_graphics"
+  description: "Flag for bufferstreams buffer pooling milestone"
+  bug: "296101127"
+}
+
+flag {
+  name: "bufferstreams_ipc"
+  namespace: "core_graphics"
+  description: "Flag for bufferstreams IPC milestone"
+  bug: "296099728"
+}
+
+flag {
+  name: "bufferstreams_cpp"
+  namespace: "core_graphics"
+  description: "Flag for bufferstreams C/C++ milestone"
+  bug: "296100536"
+}
+
+flag {
+  name: "bufferstreams_utils"
+  namespace: "core_graphics"
+  description: "Flag for bufferstreams extra utilities milestone"
+  bug: "285322189"
+}
+
+flag {
+  name: "bufferstreams_demo"
+  namespace: "core_graphics"
+  description: "Flag for bufferstreams demo milestone"
+  bug: "297242965"
+}
+
+flag {
+  name: "bufferstreams_perf"
+  namespace: "core_graphics"
+  description: "Flag for bufferstreams performance enhancement milestone"
+  bug: "297242843"
+}
+
+flag {
+  name: "bufferstreams_tooling"
+  namespace: "core_graphics"
+  description: "Flag for bufferstreams tooling milestone"
+  bug: "297243180"
+}
+
diff --git a/libs/fakeservicemanager/Android.bp b/libs/fakeservicemanager/Android.bp
index 96dcce1..3823393 100644
--- a/libs/fakeservicemanager/Android.bp
+++ b/libs/fakeservicemanager/Android.bp
@@ -17,6 +17,7 @@
     shared_libs: [
         "libbinder",
         "libutils",
+        "liblog",
     ],
     target: {
         darwin: {
@@ -40,3 +41,41 @@
     static_libs: ["libgmock"],
     local_include_dirs: ["include"],
 }
+
+rust_bindgen {
+    name: "libfakeservicemanager_bindgen",
+    crate_name: "fakeservicemanager_bindgen",
+    host_supported: true,
+    wrapper_src: "rust/wrappers/FakeServiceManagerWrapper.hpp",
+    source_stem: "bindings",
+    visibility: [":__subpackages__"],
+    bindgen_flags: [
+        "--allowlist-function",
+        "setupFakeServiceManager",
+        "--allowlist-function",
+        "clearFakeServiceManager",
+    ],
+    shared_libs: [
+        "libc++",
+        "libbinder",
+        "libfakeservicemanager",
+    ],
+}
+
+rust_library {
+    name: "libfakeservicemanager_rs",
+    crate_name: "fakeservicemanager_rs",
+    host_supported: true,
+    srcs: [
+        "rust/src/lib.rs",
+    ],
+    shared_libs: [
+        "libc++",
+        "libfakeservicemanager",
+    ],
+    rustlibs: [
+        "libfakeservicemanager_bindgen",
+    ],
+    lints: "none",
+    clippy_lints: "none",
+}
diff --git a/libs/fakeservicemanager/FakeServiceManager.cpp b/libs/fakeservicemanager/FakeServiceManager.cpp
index 80661c1..ae242f3 100644
--- a/libs/fakeservicemanager/FakeServiceManager.cpp
+++ b/libs/fakeservicemanager/FakeServiceManager.cpp
@@ -16,6 +16,10 @@
 
 #include "fakeservicemanager/FakeServiceManager.h"
 
+using android::sp;
+using android::FakeServiceManager;
+using android::setDefaultServiceManager;
+
 namespace android {
 
 FakeServiceManager::FakeServiceManager() {}
@@ -80,7 +84,7 @@
     for (const auto& [registeredName, service] : mNameToService) {
         (void) service;
         if (registeredName.startsWith(prefix)) {
-            out.add(String16(registeredName.string() + prefix.size()));
+            out.add(String16(registeredName.c_str() + prefix.size()));
         }
     }
     return out;
@@ -123,3 +127,24 @@
     mNameToService.clear();
 }
 }  // namespace android
+
+[[clang::no_destroy]] static sp<FakeServiceManager> gFakeServiceManager;
+[[clang::no_destroy]] static std::once_flag gSmOnce;
+
+extern "C" {
+
+// Setup FakeServiceManager to mock dependencies in test using this API for rust backend
+void setupFakeServiceManager() {
+    /* Create a FakeServiceManager instance and add required services */
+    std::call_once(gSmOnce, [&]() {
+        gFakeServiceManager = new FakeServiceManager();
+        android::setDefaultServiceManager(gFakeServiceManager);
+    });
+}
+
+// Clear existing services from Fake SM for rust backend
+void clearFakeServiceManager() {
+    LOG_ALWAYS_FATAL_IF(gFakeServiceManager == nullptr, "Fake Service Manager is not available. Forgot to call setupFakeServiceManager?");
+    gFakeServiceManager->clear();
+}
+} //extern "C"
\ No newline at end of file
diff --git a/libs/fakeservicemanager/rust/src/lib.rs b/libs/fakeservicemanager/rust/src/lib.rs
new file mode 100644
index 0000000..5b7e756
--- /dev/null
+++ b/libs/fakeservicemanager/rust/src/lib.rs
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 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.
+ */
+
+use fakeservicemanager_bindgen::{clearFakeServiceManager, setupFakeServiceManager};
+// Setup FakeServiceManager for testing and fuzzing purposes
+pub fn setup_fake_service_manager() {
+    unsafe {
+        // Safety: This API creates a new FakeSm object which will be always valid and sets up
+        // defaultServiceManager
+        setupFakeServiceManager();
+    }
+}
+
+// Setup FakeServiceManager for testing and fuzzing purposes
+pub fn clear_fake_service_manager() {
+    unsafe {
+        // Safety: This API clears all registered services with Fake SM. This should be only used
+        // setupFakeServiceManager is already called.
+        clearFakeServiceManager();
+    }
+}
diff --git a/libs/ui/include_types/ui/DataspaceUtils.h b/libs/fakeservicemanager/rust/wrappers/FakeServiceManagerWrapper.hpp
similarity index 60%
copy from libs/ui/include_types/ui/DataspaceUtils.h
copy to libs/fakeservicemanager/rust/wrappers/FakeServiceManagerWrapper.hpp
index a461cb4..1f5923a 100644
--- a/libs/ui/include_types/ui/DataspaceUtils.h
+++ b/libs/fakeservicemanager/rust/wrappers/FakeServiceManagerWrapper.hpp
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * Copyright (C) 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.
@@ -14,16 +14,12 @@
  * limitations under the License.
  */
 
-#pragma once
+#include "fakeservicemanager/FakeServiceManager.h"
 
-#include <ui/GraphicTypes.h>
+extern "C" {
+    // Setup FakeServiceManager to mock dependencies in test using this API
+    void setupFakeServiceManager();
 
-namespace android {
-
-inline bool isHdrDataspace(ui::Dataspace dataspace) {
-    const auto transfer = dataspace & HAL_DATASPACE_TRANSFER_MASK;
-
-    return transfer == HAL_DATASPACE_TRANSFER_ST2084 || transfer == HAL_DATASPACE_TRANSFER_HLG;
-}
-
-} // namespace android
\ No newline at end of file
+    // Clear existing services from Fake SM.
+    void clearFakeServiceManager();
+} // extern "C"
diff --git a/libs/fakeservicemanager/test_sm.cpp b/libs/fakeservicemanager/test_sm.cpp
index 6fc21c6..cb6784c0 100644
--- a/libs/fakeservicemanager/test_sm.cpp
+++ b/libs/fakeservicemanager/test_sm.cpp
@@ -22,6 +22,7 @@
 #include <binder/IServiceManager.h>
 
 #include "fakeservicemanager/FakeServiceManager.h"
+#include "rust/wrappers/FakeServiceManagerWrapper.hpp"
 
 using android::sp;
 using android::BBinder;
@@ -31,6 +32,7 @@
 using android::FakeServiceManager;
 using android::String16;
 using android::IServiceManager;
+using android::defaultServiceManager;
 using testing::ElementsAre;
 
 static sp<IBinder> getBinder() {
@@ -83,7 +85,7 @@
     EXPECT_EQ(sm->getService(String16("foo")), service);
 }
 
-TEST(GetService, NonExistant) {
+TEST(GetService, NonExistent) {
     auto sm = new FakeServiceManager();
 
     EXPECT_EQ(sm->getService(String16("foo")), nullptr);
@@ -108,7 +110,7 @@
         String16("sd")));
 }
 
-TEST(WaitForService, NonExistant) {
+TEST(WaitForService, NonExistent) {
     auto sm = new FakeServiceManager();
 
     EXPECT_EQ(sm->waitForService(String16("foo")), nullptr);
@@ -124,7 +126,7 @@
     EXPECT_EQ(sm->waitForService(String16("foo")), service);
 }
 
-TEST(IsDeclared, NonExistant) {
+TEST(IsDeclared, NonExistent) {
     auto sm = new FakeServiceManager();
 
     EXPECT_FALSE(sm->isDeclared(String16("foo")));
@@ -139,3 +141,31 @@
 
     EXPECT_TRUE(sm->isDeclared(String16("foo")));
 }
+
+TEST(SetupFakeServiceManager, NonExistent) {
+    setupFakeServiceManager();
+
+    EXPECT_EQ(defaultServiceManager()->getService(String16("foo")), nullptr);
+}
+
+TEST(SetupFakeServiceManager, GetExistingService) {
+    setupFakeServiceManager();
+    sp<IBinder> service = getBinder();
+
+    EXPECT_EQ(defaultServiceManager()->addService(String16("foo"), service, false /*allowIsolated*/,
+    IServiceManager::DUMP_FLAG_PRIORITY_DEFAULT), OK);
+
+    EXPECT_EQ(defaultServiceManager()->getService(String16("foo")), service);
+    clearFakeServiceManager();
+}
+
+TEST(ClearFakeServiceManager, GetServiceAfterClear) {
+    setupFakeServiceManager();
+
+    sp<IBinder> service = getBinder();
+    EXPECT_EQ(defaultServiceManager()->addService(String16("foo"), service, false /*allowIsolated*/,
+    IServiceManager::DUMP_FLAG_PRIORITY_DEFAULT), OK);
+
+    clearFakeServiceManager();
+    EXPECT_EQ(defaultServiceManager()->getService(String16("foo")), nullptr);
+}
\ No newline at end of file
diff --git a/libs/graphicsenv/GraphicsEnv.cpp b/libs/graphicsenv/GraphicsEnv.cpp
index 732ca36..4842476 100644
--- a/libs/graphicsenv/GraphicsEnv.cpp
+++ b/libs/graphicsenv/GraphicsEnv.cpp
@@ -60,6 +60,17 @@
 typedef bool (*fpANGLEFreeRulesHandle)(void* handle);
 typedef bool (*fpANGLEFreeSystemInfoHandle)(void* handle);
 
+namespace {
+static bool isVndkEnabled() {
+#ifdef __BIONIC__
+    // TODO(b/290159430) Use ro.vndk.version to check if VNDK is enabled instead
+    static bool isVndkEnabled = !android::base::GetBoolProperty("ro.vndk.deprecate", false);
+    return isVndkEnabled;
+#endif
+    return false;
+}
+} // namespace
+
 namespace android {
 
 enum NativeLibrary {
@@ -71,6 +82,8 @@
         {"/apex/com.android.vndk.v{}/etc/llndk.libraries.{}.txt",
          "/apex/com.android.vndk.v{}/etc/vndksp.libraries.{}.txt"};
 
+static const char* kLlndkLibrariesTxtPath = "/system/etc/llndk.libraries.txt";
+
 static std::string vndkVersionStr() {
 #ifdef __BIONIC__
     return base::GetProperty("ro.vndk.version", "");
@@ -108,8 +121,14 @@
 }
 
 static const std::string getSystemNativeLibraries(NativeLibrary type) {
-    std::string nativeLibrariesSystemConfig = kNativeLibrariesSystemConfigPath[type];
-    insertVndkVersionStr(&nativeLibrariesSystemConfig);
+    std::string nativeLibrariesSystemConfig = "";
+
+    if (!isVndkEnabled() && type == NativeLibrary::LLNDK) {
+        nativeLibrariesSystemConfig = kLlndkLibrariesTxtPath;
+    } else {
+        nativeLibrariesSystemConfig = kNativeLibrariesSystemConfigPath[type];
+        insertVndkVersionStr(&nativeLibrariesSystemConfig);
+    }
 
     std::vector<std::string> soNames;
     if (!readConfig(nativeLibrariesSystemConfig, &soNames)) {
diff --git a/libs/gui/BufferItemConsumer.cpp b/libs/gui/BufferItemConsumer.cpp
index f50bc20..e6331e7 100644
--- a/libs/gui/BufferItemConsumer.cpp
+++ b/libs/gui/BufferItemConsumer.cpp
@@ -24,11 +24,11 @@
 #include <gui/BufferItem.h>
 #include <gui/BufferItemConsumer.h>
 
-#define BI_LOGV(x, ...) ALOGV("[%s] " x, mName.string(), ##__VA_ARGS__)
-//#define BI_LOGD(x, ...) ALOGD("[%s] " x, mName.string(), ##__VA_ARGS__)
-//#define BI_LOGI(x, ...) ALOGI("[%s] " x, mName.string(), ##__VA_ARGS__)
-//#define BI_LOGW(x, ...) ALOGW("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define BI_LOGE(x, ...) ALOGE("[%s] " x, mName.string(), ##__VA_ARGS__)
+#define BI_LOGV(x, ...) ALOGV("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+// #define BI_LOGD(x, ...) ALOGD("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+// #define BI_LOGI(x, ...) ALOGI("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+// #define BI_LOGW(x, ...) ALOGW("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define BI_LOGE(x, ...) ALOGE("[%s] " x, mName.c_str(), ##__VA_ARGS__)
 
 namespace android {
 
diff --git a/libs/gui/BufferQueueConsumer.cpp b/libs/gui/BufferQueueConsumer.cpp
index 5217209..5b34ba1 100644
--- a/libs/gui/BufferQueueConsumer.cpp
+++ b/libs/gui/BufferQueueConsumer.cpp
@@ -47,23 +47,23 @@
 
 // Macros for include BufferQueueCore information in log messages
 #define BQ_LOGV(x, ...)                                                                           \
-    ALOGV("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGV("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 #define BQ_LOGD(x, ...)                                                                           \
-    ALOGD("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGD("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 #define BQ_LOGI(x, ...)                                                                           \
-    ALOGI("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGI("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 #define BQ_LOGW(x, ...)                                                                           \
-    ALOGW("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGW("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 #define BQ_LOGE(x, ...)                                                                           \
-    ALOGE("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGE("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 
@@ -298,8 +298,7 @@
         // decrease.
         mCore->mDequeueCondition.notify_all();
 
-        ATRACE_INT(mCore->mConsumerName.string(),
-                static_cast<int32_t>(mCore->mQueue.size()));
+        ATRACE_INT(mCore->mConsumerName.c_str(), static_cast<int32_t>(mCore->mQueue.size()));
 #ifndef NO_BINDER
         mCore->mOccupancyTracker.registerOccupancyChange(mCore->mQueue.size());
 #endif
@@ -718,7 +717,7 @@
 
 status_t BufferQueueConsumer::setConsumerName(const String8& name) {
     ATRACE_CALL();
-    BQ_LOGV("setConsumerName: '%s'", name.string());
+    BQ_LOGV("setConsumerName: '%s'", name.c_str());
     std::lock_guard<std::mutex> lock(mCore->mMutex);
     mCore->mConsumerName = name;
     mConsumerName = name;
diff --git a/libs/gui/BufferQueueCore.cpp b/libs/gui/BufferQueueCore.cpp
index 2930154..648db67 100644
--- a/libs/gui/BufferQueueCore.cpp
+++ b/libs/gui/BufferQueueCore.cpp
@@ -41,20 +41,20 @@
 namespace android {
 
 // Macros for include BufferQueueCore information in log messages
-#define BQ_LOGV(x, ...)                                                                           \
-    ALOGV("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(), mUniqueId, \
+#define BQ_LOGV(x, ...)                                                                          \
+    ALOGV("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(), mUniqueId, \
           mConnectedApi, mConnectedPid, mUniqueId >> 32, ##__VA_ARGS__)
-#define BQ_LOGD(x, ...)                                                                           \
-    ALOGD("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(), mUniqueId, \
+#define BQ_LOGD(x, ...)                                                                          \
+    ALOGD("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(), mUniqueId, \
           mConnectedApi, mConnectedPid, mUniqueId >> 32, ##__VA_ARGS__)
-#define BQ_LOGI(x, ...)                                                                           \
-    ALOGI("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(), mUniqueId, \
+#define BQ_LOGI(x, ...)                                                                          \
+    ALOGI("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(), mUniqueId, \
           mConnectedApi, mConnectedPid, mUniqueId >> 32, ##__VA_ARGS__)
-#define BQ_LOGW(x, ...)                                                                           \
-    ALOGW("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(), mUniqueId, \
+#define BQ_LOGW(x, ...)                                                                          \
+    ALOGW("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(), mUniqueId, \
           mConnectedApi, mConnectedPid, mUniqueId >> 32, ##__VA_ARGS__)
-#define BQ_LOGE(x, ...)                                                                           \
-    ALOGE("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(), mUniqueId, \
+#define BQ_LOGE(x, ...)                                                                          \
+    ALOGE("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(), mUniqueId, \
           mConnectedApi, mConnectedPid, mUniqueId >> 32, ##__VA_ARGS__)
 
 static String8 getUniqueName() {
@@ -146,23 +146,23 @@
 void BufferQueueCore::dumpState(const String8& prefix, String8* outResult) const {
     std::lock_guard<std::mutex> lock(mMutex);
 
-    outResult->appendFormat("%s- BufferQueue ", prefix.string());
+    outResult->appendFormat("%s- BufferQueue ", prefix.c_str());
     outResult->appendFormat("mMaxAcquiredBufferCount=%d mMaxDequeuedBufferCount=%d\n",
                             mMaxAcquiredBufferCount, mMaxDequeuedBufferCount);
-    outResult->appendFormat("%s  mDequeueBufferCannotBlock=%d mAsyncMode=%d\n", prefix.string(),
+    outResult->appendFormat("%s  mDequeueBufferCannotBlock=%d mAsyncMode=%d\n", prefix.c_str(),
                             mDequeueBufferCannotBlock, mAsyncMode);
-    outResult->appendFormat("%s  mQueueBufferCanDrop=%d mLegacyBufferDrop=%d\n", prefix.string(),
+    outResult->appendFormat("%s  mQueueBufferCanDrop=%d mLegacyBufferDrop=%d\n", prefix.c_str(),
                             mQueueBufferCanDrop, mLegacyBufferDrop);
-    outResult->appendFormat("%s  default-size=[%dx%d] default-format=%d ", prefix.string(),
+    outResult->appendFormat("%s  default-size=[%dx%d] default-format=%d ", prefix.c_str(),
                             mDefaultWidth, mDefaultHeight, mDefaultBufferFormat);
-    outResult->appendFormat("%s  transform-hint=%02x frame-counter=%" PRIu64 "\n", prefix.string(),
+    outResult->appendFormat("%s  transform-hint=%02x frame-counter=%" PRIu64 "\n", prefix.c_str(),
                             mTransformHint, mFrameCounter);
-    outResult->appendFormat("%s  mTransformHintInUse=%02x mAutoPrerotation=%d\n", prefix.string(),
+    outResult->appendFormat("%s  mTransformHintInUse=%02x mAutoPrerotation=%d\n", prefix.c_str(),
                             mTransformHintInUse, mAutoPrerotation);
 
-    outResult->appendFormat("%sFIFO(%zu):\n", prefix.string(), mQueue.size());
+    outResult->appendFormat("%sFIFO(%zu):\n", prefix.c_str(), mQueue.size());
 
-    outResult->appendFormat("%s(mConsumerName=%s, ", prefix.string(), mConsumerName.string());
+    outResult->appendFormat("%s(mConsumerName=%s, ", prefix.c_str(), mConsumerName.c_str());
 
     outResult->appendFormat("mConnectedApi=%d, mConsumerUsageBits=%" PRIu64 ", ", mConnectedApi,
                             mConsumerUsageBits);
@@ -173,12 +173,11 @@
     getProcessName(mConnectedPid, producerProcName);
     getProcessName(pid, consumerProcName);
     outResult->appendFormat("mId=%" PRIx64 ", producer=[%d:%s], consumer=[%d:%s])\n", mUniqueId,
-                            mConnectedPid, producerProcName.string(), pid,
-                            consumerProcName.string());
+                            mConnectedPid, producerProcName.c_str(), pid, consumerProcName.c_str());
     Fifo::const_iterator current(mQueue.begin());
     while (current != mQueue.end()) {
         double timestamp = current->mTimestamp / 1e9;
-        outResult->appendFormat("%s  %02d:%p ", prefix.string(), current->mSlot,
+        outResult->appendFormat("%s  %02d:%p ", prefix.c_str(), current->mSlot,
                                 current->mGraphicBuffer.get());
         outResult->appendFormat("crop=[%d,%d,%d,%d] ", current->mCrop.left, current->mCrop.top,
                                 current->mCrop.right, current->mCrop.bottom);
@@ -187,12 +186,12 @@
         ++current;
     }
 
-    outResult->appendFormat("%sSlots:\n", prefix.string());
+    outResult->appendFormat("%sSlots:\n", prefix.c_str());
     for (int s : mActiveBuffers) {
         const sp<GraphicBuffer>& buffer(mSlots[s].mGraphicBuffer);
         // A dequeued buffer might be null if it's still being allocated
         if (buffer.get()) {
-            outResult->appendFormat("%s %s[%02d:%p] ", prefix.string(),
+            outResult->appendFormat("%s %s[%02d:%p] ", prefix.c_str(),
                                     (mSlots[s].mBufferState.isAcquired()) ? ">" : " ", s,
                                     buffer.get());
             outResult->appendFormat("state=%-8s %p frame=%" PRIu64, mSlots[s].mBufferState.string(),
@@ -200,14 +199,14 @@
             outResult->appendFormat(" [%4ux%4u:%4u,%3X]\n", buffer->width, buffer->height,
                                     buffer->stride, buffer->format);
         } else {
-            outResult->appendFormat("%s  [%02d:%p] ", prefix.string(), s, buffer.get());
+            outResult->appendFormat("%s  [%02d:%p] ", prefix.c_str(), s, buffer.get());
             outResult->appendFormat("state=%-8s frame=%" PRIu64 "\n",
                                     mSlots[s].mBufferState.string(), mSlots[s].mFrameNumber);
         }
     }
     for (int s : mFreeBuffers) {
         const sp<GraphicBuffer>& buffer(mSlots[s].mGraphicBuffer);
-        outResult->appendFormat("%s  [%02d:%p] ", prefix.string(), s, buffer.get());
+        outResult->appendFormat("%s  [%02d:%p] ", prefix.c_str(), s, buffer.get());
         outResult->appendFormat("state=%-8s %p frame=%" PRIu64, mSlots[s].mBufferState.string(),
                                 buffer->handle, mSlots[s].mFrameNumber);
         outResult->appendFormat(" [%4ux%4u:%4u,%3X]\n", buffer->width, buffer->height,
@@ -216,7 +215,7 @@
 
     for (int s : mFreeSlots) {
         const sp<GraphicBuffer>& buffer(mSlots[s].mGraphicBuffer);
-        outResult->appendFormat("%s  [%02d:%p] state=%-8s\n", prefix.string(), s, buffer.get(),
+        outResult->appendFormat("%s  [%02d:%p] state=%-8s\n", prefix.c_str(), s, buffer.get(),
                                 mSlots[s].mBufferState.string());
     }
 }
diff --git a/libs/gui/BufferQueueProducer.cpp b/libs/gui/BufferQueueProducer.cpp
index b872541..920b83d 100644
--- a/libs/gui/BufferQueueProducer.cpp
+++ b/libs/gui/BufferQueueProducer.cpp
@@ -47,23 +47,23 @@
 
 // Macros for include BufferQueueCore information in log messages
 #define BQ_LOGV(x, ...)                                                                           \
-    ALOGV("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGV("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 #define BQ_LOGD(x, ...)                                                                           \
-    ALOGD("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGD("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 #define BQ_LOGI(x, ...)                                                                           \
-    ALOGI("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGI("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 #define BQ_LOGW(x, ...)                                                                           \
-    ALOGW("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGW("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 #define BQ_LOGE(x, ...)                                                                           \
-    ALOGE("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.string(),            \
+    ALOGE("[%s](id:%" PRIx64 ",api:%d,p:%d,c:%" PRIu64 ") " x, mConsumerName.c_str(),             \
           mCore->mUniqueId, mCore->mConnectedApi, mCore->mConnectedPid, (mCore->mUniqueId) >> 32, \
           ##__VA_ARGS__)
 
@@ -508,13 +508,13 @@
         {
             if (CC_UNLIKELY(ATRACE_ENABLED())) {
                 if (buffer == nullptr) {
-                    ATRACE_FORMAT_INSTANT("%s buffer reallocation: null", mConsumerName.string());
+                    ATRACE_FORMAT_INSTANT("%s buffer reallocation: null", mConsumerName.c_str());
                 } else {
                     ATRACE_FORMAT_INSTANT("%s buffer reallocation actual %dx%d format:%d "
                                           "layerCount:%d "
                                           "usage:%d requested: %dx%d format:%d layerCount:%d "
                                           "usage:%d ",
-                                          mConsumerName.string(), width, height, format,
+                                          mConsumerName.c_str(), width, height, format,
                                           BQ_LAYER_COUNT, usage, buffer->getWidth(),
                                           buffer->getHeight(), buffer->getPixelFormat(),
                                           buffer->getLayerCount(), buffer->getUsage());
@@ -573,9 +573,9 @@
 
     if (returnFlags & BUFFER_NEEDS_REALLOCATION) {
         BQ_LOGV("dequeueBuffer: allocating a new buffer for slot %d", *outSlot);
-        sp<GraphicBuffer> graphicBuffer = new GraphicBuffer(
-                width, height, format, BQ_LAYER_COUNT, usage,
-                {mConsumerName.string(), mConsumerName.size()});
+        sp<GraphicBuffer> graphicBuffer =
+                new GraphicBuffer(width, height, format, BQ_LAYER_COUNT, usage,
+                                  {mConsumerName.c_str(), mConsumerName.size()});
 
         status_t error = graphicBuffer->initCheck();
 
@@ -1046,8 +1046,7 @@
         output->numPendingBuffers = static_cast<uint32_t>(mCore->mQueue.size());
         output->nextFrameNumber = mCore->mFrameCounter + 1;
 
-        ATRACE_INT(mCore->mConsumerName.string(),
-                static_cast<int32_t>(mCore->mQueue.size()));
+        ATRACE_INT(mCore->mConsumerName.c_str(), static_cast<int32_t>(mCore->mQueue.size()));
 #ifndef NO_BINDER
         mCore->mOccupancyTracker.registerOccupancyChange(mCore->mQueue.size());
 #endif
@@ -1487,7 +1486,7 @@
 
             allocFormat = format != 0 ? format : mCore->mDefaultBufferFormat;
             allocUsage = usage | mCore->mConsumerUsageBits;
-            allocName.assign(mCore->mConsumerName.string(), mCore->mConsumerName.size());
+            allocName.assign(mCore->mConsumerName.c_str(), mCore->mConsumerName.size());
 
             mCore->mIsAllocating = true;
         } // Autolock scope
@@ -1589,7 +1588,7 @@
 String8 BufferQueueProducer::getConsumerName() const {
     ATRACE_CALL();
     std::lock_guard<std::mutex> lock(mCore->mMutex);
-    BQ_LOGV("getConsumerName: %s", mConsumerName.string());
+    BQ_LOGV("getConsumerName: %s", mConsumerName.c_str());
     return mConsumerName;
 }
 
diff --git a/libs/gui/ConsumerBase.cpp b/libs/gui/ConsumerBase.cpp
index 9f91d9d..b625c3f 100644
--- a/libs/gui/ConsumerBase.cpp
+++ b/libs/gui/ConsumerBase.cpp
@@ -41,11 +41,11 @@
 #include <utils/Trace.h>
 
 // Macros for including the ConsumerBase name in log messages
-#define CB_LOGV(x, ...) ALOGV("[%s] " x, mName.string(), ##__VA_ARGS__)
-//#define CB_LOGD(x, ...) ALOGD("[%s] " x, mName.string(), ##__VA_ARGS__)
-//#define CB_LOGI(x, ...) ALOGI("[%s] " x, mName.string(), ##__VA_ARGS__)
-//#define CB_LOGW(x, ...) ALOGW("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define CB_LOGE(x, ...) ALOGE("[%s] " x, mName.string(), ##__VA_ARGS__)
+#define CB_LOGV(x, ...) ALOGV("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+// #define CB_LOGD(x, ...) ALOGD("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+// #define CB_LOGI(x, ...) ALOGI("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+// #define CB_LOGW(x, ...) ALOGW("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define CB_LOGE(x, ...) ALOGE("[%s] " x, mName.c_str(), ##__VA_ARGS__)
 
 namespace android {
 
@@ -86,8 +86,10 @@
     // be done by ConsumerBase::onLastStrongRef(), but it's possible for a
     // derived class to override that method and not call
     // ConsumerBase::onLastStrongRef().
-    LOG_ALWAYS_FATAL_IF(!mAbandoned, "[%s] ~ConsumerBase was called, but the "
-        "consumer is not abandoned!", mName.string());
+    LOG_ALWAYS_FATAL_IF(!mAbandoned,
+                        "[%s] ~ConsumerBase was called, but the "
+                        "consumer is not abandoned!",
+                        mName.c_str());
 }
 
 void ConsumerBase::onLastStrongRef(const void* id __attribute__((unused))) {
@@ -451,7 +453,7 @@
     // them to get an accurate timestamp.
     if (currentStatus == incomingStatus) {
         char fenceName[32] = {};
-        snprintf(fenceName, 32, "%.28s:%d", mName.string(), slot);
+        snprintf(fenceName, 32, "%.28s:%d", mName.c_str(), slot);
         sp<Fence> mergedFence = Fence::merge(
                 fenceName, mSlots[slot].mFence, fence);
         if (!mergedFence.get()) {
diff --git a/libs/gui/CpuConsumer.cpp b/libs/gui/CpuConsumer.cpp
index a626970..3031fa1 100644
--- a/libs/gui/CpuConsumer.cpp
+++ b/libs/gui/CpuConsumer.cpp
@@ -23,11 +23,11 @@
 #include <gui/BufferItem.h>
 #include <utils/Log.h>
 
-#define CC_LOGV(x, ...) ALOGV("[%s] " x, mName.string(), ##__VA_ARGS__)
-//#define CC_LOGD(x, ...) ALOGD("[%s] " x, mName.string(), ##__VA_ARGS__)
-//#define CC_LOGI(x, ...) ALOGI("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define CC_LOGW(x, ...) ALOGW("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define CC_LOGE(x, ...) ALOGE("[%s] " x, mName.string(), ##__VA_ARGS__)
+#define CC_LOGV(x, ...) ALOGV("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+// #define CC_LOGD(x, ...) ALOGD("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+// #define CC_LOGI(x, ...) ALOGI("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define CC_LOGW(x, ...) ALOGW("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define CC_LOGE(x, ...) ALOGE("[%s] " x, mName.c_str(), ##__VA_ARGS__)
 
 namespace android {
 
diff --git a/libs/gui/GLConsumer.cpp b/libs/gui/GLConsumer.cpp
index b3647d6..d49489c 100644
--- a/libs/gui/GLConsumer.cpp
+++ b/libs/gui/GLConsumer.cpp
@@ -52,11 +52,11 @@
 namespace android {
 
 // Macros for including the GLConsumer name in log messages
-#define GLC_LOGV(x, ...) ALOGV("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define GLC_LOGD(x, ...) ALOGD("[%s] " x, mName.string(), ##__VA_ARGS__)
-//#define GLC_LOGI(x, ...) ALOGI("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define GLC_LOGW(x, ...) ALOGW("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define GLC_LOGE(x, ...) ALOGE("[%s] " x, mName.string(), ##__VA_ARGS__)
+#define GLC_LOGV(x, ...) ALOGV("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define GLC_LOGD(x, ...) ALOGD("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+// #define GLC_LOGI(x, ...) ALOGI("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define GLC_LOGW(x, ...) ALOGW("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define GLC_LOGE(x, ...) ALOGE("[%s] " x, mName.c_str(), ##__VA_ARGS__)
 
 static const struct {
     uint32_t width, height;
diff --git a/libs/gui/LayerState.cpp b/libs/gui/LayerState.cpp
index 2322b70..e1afb52 100644
--- a/libs/gui/LayerState.cpp
+++ b/libs/gui/LayerState.cpp
@@ -83,6 +83,7 @@
         frameRateCompatibility(ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT),
         changeFrameRateStrategy(ANATIVEWINDOW_CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS),
         defaultFrameRateCompatibility(ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT),
+        frameRateCategory(ANATIVEWINDOW_FRAME_RATE_CATEGORY_DEFAULT),
         fixedTransformHint(ui::Transform::ROT_INVALID),
         autoRefresh(false),
         isTrustedOverlay(false),
@@ -158,6 +159,7 @@
     SAFE_PARCEL(output.writeByte, frameRateCompatibility);
     SAFE_PARCEL(output.writeByte, changeFrameRateStrategy);
     SAFE_PARCEL(output.writeByte, defaultFrameRateCompatibility);
+    SAFE_PARCEL(output.writeByte, frameRateCategory);
     SAFE_PARCEL(output.writeUint32, fixedTransformHint);
     SAFE_PARCEL(output.writeBool, autoRefresh);
     SAFE_PARCEL(output.writeBool, dimmingEnabled);
@@ -290,6 +292,7 @@
     SAFE_PARCEL(input.readByte, &frameRateCompatibility);
     SAFE_PARCEL(input.readByte, &changeFrameRateStrategy);
     SAFE_PARCEL(input.readByte, &defaultFrameRateCompatibility);
+    SAFE_PARCEL(input.readByte, &frameRateCategory);
     SAFE_PARCEL(input.readUint32, &tmpUint32);
     fixedTransformHint = static_cast<ui::Transform::RotationFlags>(tmpUint32);
     SAFE_PARCEL(input.readBool, &autoRefresh);
@@ -659,6 +662,10 @@
         frameRateCompatibility = other.frameRateCompatibility;
         changeFrameRateStrategy = other.changeFrameRateStrategy;
     }
+    if (other.what & eFrameRateCategoryChanged) {
+        what |= eFrameRateCategoryChanged;
+        frameRateCategory = other.frameRateCategory;
+    }
     if (other.what & eFixedTransformHintChanged) {
         what |= eFixedTransformHintChanged;
         fixedTransformHint = other.fixedTransformHint;
@@ -769,6 +776,7 @@
     CHECK_DIFF(diff, eFrameRateSelectionPriority, other, frameRateSelectionPriority);
     CHECK_DIFF3(diff, eFrameRateChanged, other, frameRate, frameRateCompatibility,
                 changeFrameRateStrategy);
+    CHECK_DIFF(diff, eFrameRateCategoryChanged, other, frameRateCategory);
     CHECK_DIFF(diff, eFixedTransformHintChanged, other, fixedTransformHint);
     CHECK_DIFF(diff, eAutoRefreshChanged, other, autoRefresh);
     CHECK_DIFF(diff, eTrustedOverlayChanged, other, isTrustedOverlay);
diff --git a/libs/gui/SurfaceComposerClient.cpp b/libs/gui/SurfaceComposerClient.cpp
index 7840120..4db960e 100644
--- a/libs/gui/SurfaceComposerClient.cpp
+++ b/libs/gui/SurfaceComposerClient.cpp
@@ -1275,7 +1275,7 @@
     sp<IBinder> display = nullptr;
     binder::Status status =
             ComposerServiceAIDL::getComposerService()->createDisplay(std::string(
-                                                                             displayName.string()),
+                                                                             displayName.c_str()),
                                                                      secure, requestedRefereshRate,
                                                                      &display);
     return status.isOk() ? display : nullptr;
@@ -2092,6 +2092,18 @@
     return *this;
 }
 
+SurfaceComposerClient::Transaction& SurfaceComposerClient::Transaction::setFrameRateCategory(
+        const sp<SurfaceControl>& sc, int8_t category) {
+    layer_state_t* s = getLayerState(sc);
+    if (!s) {
+        mStatus = BAD_INDEX;
+        return *this;
+    }
+    s->what |= layer_state_t::eFrameRateCategoryChanged;
+    s->frameRateCategory = category;
+    return *this;
+}
+
 SurfaceComposerClient::Transaction& SurfaceComposerClient::Transaction::setFixedTransformHint(
         const sp<SurfaceControl>& sc, int32_t fixedTransformHint) {
     layer_state_t* s = getLayerState(sc);
@@ -2419,7 +2431,7 @@
 
     if (mStatus == NO_ERROR) {
         gui::CreateSurfaceResult result;
-        binder::Status status = mClient->createSurface(std::string(name.string()), flags,
+        binder::Status status = mClient->createSurface(std::string(name.c_str()), flags,
                                                        parentHandle, std::move(metadata), &result);
         err = statusTFromBinderStatus(status);
         if (outTransformHint) {
@@ -2756,6 +2768,20 @@
     return statusTFromBinderStatus(status);
 }
 
+status_t SurfaceComposerClient::updateSmallAreaDetection(std::vector<int32_t>& uids,
+                                                         std::vector<float>& thresholds) {
+    binder::Status status =
+            ComposerServiceAIDL::getComposerService()->updateSmallAreaDetection(uids, thresholds);
+    return statusTFromBinderStatus(status);
+}
+
+status_t SurfaceComposerClient::setSmallAreaDetectionThreshold(uid_t uid, float threshold) {
+    binder::Status status =
+            ComposerServiceAIDL::getComposerService()->setSmallAreaDetectionThreshold(uid,
+                                                                                      threshold);
+    return statusTFromBinderStatus(status);
+}
+
 void SurfaceComposerClient::setAutoLowLatencyMode(const sp<IBinder>& display, bool on) {
     ComposerServiceAIDL::getComposerService()->setAutoLowLatencyMode(display, on);
 }
diff --git a/libs/gui/WindowInfo.cpp b/libs/gui/WindowInfo.cpp
index 52af9d5..2eb6bd6 100644
--- a/libs/gui/WindowInfo.cpp
+++ b/libs/gui/WindowInfo.cpp
@@ -43,7 +43,7 @@
 }
 
 bool WindowInfo::frameContainsPoint(int32_t x, int32_t y) const {
-    return x >= frameLeft && x < frameRight && y >= frameTop && y < frameBottom;
+    return x >= frame.left && x < frame.right && y >= frame.top && y < frame.bottom;
 }
 
 bool WindowInfo::supportsSplitTouch() const {
@@ -59,18 +59,15 @@
 }
 
 bool WindowInfo::overlaps(const WindowInfo* other) const {
-    const bool nonEmpty = (frameRight - frameLeft > 0) || (frameBottom - frameTop > 0);
-    return nonEmpty && frameLeft < other->frameRight && frameRight > other->frameLeft &&
-            frameTop < other->frameBottom && frameBottom > other->frameTop;
+    return !frame.isEmpty() && frame.left < other->frame.right && frame.right > other->frame.left &&
+            frame.top < other->frame.bottom && frame.bottom > other->frame.top;
 }
 
 bool WindowInfo::operator==(const WindowInfo& info) const {
     return info.token == token && info.id == id && info.name == name &&
-            info.dispatchingTimeout == dispatchingTimeout && info.frameLeft == frameLeft &&
-            info.frameTop == frameTop && info.frameRight == frameRight &&
-            info.frameBottom == frameBottom && info.surfaceInset == surfaceInset &&
-            info.globalScaleFactor == globalScaleFactor && info.transform == transform &&
-            info.touchableRegion.hasSameRects(touchableRegion) &&
+            info.dispatchingTimeout == dispatchingTimeout && info.frame == frame &&
+            info.surfaceInset == surfaceInset && info.globalScaleFactor == globalScaleFactor &&
+            info.transform == transform && info.touchableRegion.hasSameRects(touchableRegion) &&
             info.touchOcclusionMode == touchOcclusionMode && info.ownerPid == ownerPid &&
             info.ownerUid == ownerUid && info.packageName == packageName &&
             info.inputConfig == inputConfig && info.displayId == displayId &&
@@ -103,10 +100,7 @@
         parcel->writeInt32(layoutParamsFlags.get()) ?:
         parcel->writeInt32(
                 static_cast<std::underlying_type_t<WindowInfo::Type>>(layoutParamsType)) ?:
-        parcel->writeInt32(frameLeft) ?:
-        parcel->writeInt32(frameTop) ?:
-        parcel->writeInt32(frameRight) ?:
-        parcel->writeInt32(frameBottom) ?:
+        parcel->write(frame) ?:
         parcel->writeInt32(surfaceInset) ?:
         parcel->writeFloat(globalScaleFactor) ?:
         parcel->writeFloat(alpha) ?:
@@ -155,10 +149,7 @@
     // clang-format off
     status = parcel->readInt32(&lpFlags) ?:
         parcel->readInt32(&lpType) ?:
-        parcel->readInt32(&frameLeft) ?:
-        parcel->readInt32(&frameTop) ?:
-        parcel->readInt32(&frameRight) ?:
-        parcel->readInt32(&frameBottom) ?:
+        parcel->read(frame) ?:
         parcel->readInt32(&surfaceInset) ?:
         parcel->readFloat(&globalScaleFactor) ?:
         parcel->readFloat(&alpha) ?:
diff --git a/libs/gui/aidl/android/gui/ISurfaceComposer.aidl b/libs/gui/aidl/android/gui/ISurfaceComposer.aidl
index e1726b7..1c604a1 100644
--- a/libs/gui/aidl/android/gui/ISurfaceComposer.aidl
+++ b/libs/gui/aidl/android/gui/ISurfaceComposer.aidl
@@ -479,6 +479,39 @@
      */
     void setOverrideFrameRate(int uid, float frameRate);
 
+    oneway void updateSmallAreaDetection(in int[] uids, in float[] thresholds);
+
+    /**
+     * Set the small area detection threshold for a specified uid by SmallAreaDetectionController.
+     * Passing the threshold and uid to SurfaceFlinger to update the uid-threshold mapping
+     * in the scheduler.
+     */
+    oneway void setSmallAreaDetectionThreshold(int uid, float threshold);
+
+    /**
+     * Enables or disables the frame rate overlay in the top left corner.
+     * Requires root or android.permission.HARDWARE_TEST
+     */
+    void enableRefreshRateOverlay(boolean active);
+
+    /**
+     * Enables or disables the debug flash.
+     * Requires root or android.permission.HARDWARE_TEST
+     */
+    void setDebugFlash(int delay);
+
+    /**
+     * Force composite ahead of next VSYNC.
+     * Requires root or android.permission.HARDWARE_TEST
+     */
+    void scheduleComposite();
+
+    /**
+     * Force commit ahead of next VSYNC.
+     * Requires root or android.permission.HARDWARE_TEST
+     */
+    void scheduleCommit();
+
     /**
      * Gets priority of the RenderEngine in SurfaceFlinger.
      */
diff --git a/libs/gui/fuzzer/libgui_fuzzer_utils.h b/libs/gui/fuzzer/libgui_fuzzer_utils.h
index a381687..177d5f8 100644
--- a/libs/gui/fuzzer/libgui_fuzzer_utils.h
+++ b/libs/gui/fuzzer/libgui_fuzzer_utils.h
@@ -150,6 +150,13 @@
     MOCK_METHOD(binder::Status, getDisplayDecorationSupport,
                 (const sp<IBinder>&, std::optional<gui::DisplayDecorationSupport>*), (override));
     MOCK_METHOD(binder::Status, setOverrideFrameRate, (int32_t, float), (override));
+    MOCK_METHOD(binder::Status, enableRefreshRateOverlay, (bool), (override));
+    MOCK_METHOD(binder::Status, setDebugFlash, (int), (override));
+    MOCK_METHOD(binder::Status, scheduleComposite, (), (override));
+    MOCK_METHOD(binder::Status, scheduleCommit, (), (override));
+    MOCK_METHOD(binder::Status, updateSmallAreaDetection,
+                (const std::vector<int32_t>&, const std::vector<float>&), (override));
+    MOCK_METHOD(binder::Status, setSmallAreaDetectionThreshold, (int32_t, float), (override));
     MOCK_METHOD(binder::Status, getGpuContextPriority, (int32_t*), (override));
     MOCK_METHOD(binder::Status, getMaxAcquiredBufferCount, (int32_t*), (override));
     MOCK_METHOD(binder::Status, addWindowInfosListener,
diff --git a/libs/gui/fuzzer/libgui_surfaceComposerClient_fuzzer.cpp b/libs/gui/fuzzer/libgui_surfaceComposerClient_fuzzer.cpp
index 3e37e48..4daa3be 100644
--- a/libs/gui/fuzzer/libgui_surfaceComposerClient_fuzzer.cpp
+++ b/libs/gui/fuzzer/libgui_surfaceComposerClient_fuzzer.cpp
@@ -178,10 +178,8 @@
     windowInfo->name = mFdp.ConsumeRandomLengthString(kRandomStringMaxBytes);
     windowInfo->layoutParamsFlags = mFdp.PickValueInArray(kFlags);
     windowInfo->layoutParamsType = mFdp.PickValueInArray(kType);
-    windowInfo->frameLeft = mFdp.ConsumeIntegral<int32_t>();
-    windowInfo->frameTop = mFdp.ConsumeIntegral<int32_t>();
-    windowInfo->frameRight = mFdp.ConsumeIntegral<int32_t>();
-    windowInfo->frameBottom = mFdp.ConsumeIntegral<int32_t>();
+    windowInfo->frame = Rect(mFdp.ConsumeIntegral<int32_t>(), mFdp.ConsumeIntegral<int32_t>(),
+                             mFdp.ConsumeIntegral<int32_t>(), mFdp.ConsumeIntegral<int32_t>());
     windowInfo->surfaceInset = mFdp.ConsumeIntegral<int32_t>();
     windowInfo->alpha = mFdp.ConsumeFloatingPointInRange<float>(0, 1);
     ui::Transform transform(mFdp.PickValueInArray(kOrientation));
diff --git a/libs/gui/include/gui/BufferQueueCore.h b/libs/gui/include/gui/BufferQueueCore.h
index 8d0828d..22c2be7 100644
--- a/libs/gui/include/gui/BufferQueueCore.h
+++ b/libs/gui/include/gui/BufferQueueCore.h
@@ -34,13 +34,13 @@
 #include <mutex>
 #include <condition_variable>
 
-#define ATRACE_BUFFER_INDEX(index)                                                         \
-    do {                                                                                   \
-        if (ATRACE_ENABLED()) {                                                            \
-            char ___traceBuf[1024];                                                        \
-            snprintf(___traceBuf, 1024, "%s: %d", mCore->mConsumerName.string(), (index)); \
-            android::ScopedTrace ___bufTracer(ATRACE_TAG, ___traceBuf);                    \
-        }                                                                                  \
+#define ATRACE_BUFFER_INDEX(index)                                                        \
+    do {                                                                                  \
+        if (ATRACE_ENABLED()) {                                                           \
+            char ___traceBuf[1024];                                                       \
+            snprintf(___traceBuf, 1024, "%s: %d", mCore->mConsumerName.c_str(), (index)); \
+            android::ScopedTrace ___bufTracer(ATRACE_TAG, ___traceBuf);                   \
+        }                                                                                 \
     } while (false)
 
 namespace android {
diff --git a/libs/gui/include/gui/LayerState.h b/libs/gui/include/gui/LayerState.h
index 7aa7068..2cf5123 100644
--- a/libs/gui/include/gui/LayerState.h
+++ b/libs/gui/include/gui/LayerState.h
@@ -181,7 +181,7 @@
         eRelativeLayerChanged = 0x00004000,
         eReparent = 0x00008000,
         eColorChanged = 0x00010000,
-        /* unused = 0x00020000, */
+        eFrameRateCategoryChanged = 0x00020000,
         eBufferTransformChanged = 0x00040000,
         eTransformToDisplayInverseChanged = 0x00080000,
         eCropChanged = 0x00100000,
@@ -213,7 +213,6 @@
         eTrustedOverlayChanged = 0x4000'00000000,
         eDropInputModeChanged = 0x8000'00000000,
         eExtendedRangeBrightnessChanged = 0x10000'00000000,
-
     };
 
     layer_state_t();
@@ -265,6 +264,7 @@
     // Changes affecting child states.
     static constexpr uint64_t AFFECTS_CHILDREN = layer_state_t::GEOMETRY_CHANGES |
             layer_state_t::HIERARCHY_CHANGES | layer_state_t::eAlphaChanged |
+            layer_state_t::eBackgroundBlurRadiusChanged | layer_state_t::eBlurRegionsChanged |
             layer_state_t::eColorTransformChanged | layer_state_t::eCornerRadiusChanged |
             layer_state_t::eFlagsChanged | layer_state_t::eTrustedOverlayChanged |
             layer_state_t::eFrameRateChanged | layer_state_t::eFrameRateSelectionPriority |
@@ -358,6 +358,9 @@
     // Default frame rate compatibility used to set the layer refresh rate votetype.
     int8_t defaultFrameRateCompatibility;
 
+    // Frame rate category to suggest what frame rate range a surface should run.
+    int8_t frameRateCategory;
+
     // Set by window manager indicating the layer and all its children are
     // in a different orientation than the display. The hint suggests that
     // the graphic producers should receive a transform hint as if the
diff --git a/libs/gui/include/gui/SurfaceComposerClient.h b/libs/gui/include/gui/SurfaceComposerClient.h
index dbcbd3b..6fef5d2 100644
--- a/libs/gui/include/gui/SurfaceComposerClient.h
+++ b/libs/gui/include/gui/SurfaceComposerClient.h
@@ -203,6 +203,16 @@
     // by GameManager.
     static status_t setOverrideFrameRate(uid_t uid, float frameRate);
 
+    // Update the small area detection whole uid-threshold mappings by same size uid and threshold
+    // vector.
+    // Ref:setSmallAreaDetectionThreshold.
+    static status_t updateSmallAreaDetection(std::vector<int32_t>& uids,
+                                             std::vector<float>& thresholds);
+
+    // Sets the small area detection threshold to particular apps (uid). Passing value 0 means
+    // to disable small area detection to the app.
+    static status_t setSmallAreaDetectionThreshold(uid_t uid, float threshold);
+
     // Switches on/off Auto Low Latency Mode on the connected display. This should only be
     // called if the connected display supports Auto Low Latency Mode as reported by
     // #getAutoLowLatencyModeSupport
@@ -675,6 +685,8 @@
         Transaction& setDefaultFrameRateCompatibility(const sp<SurfaceControl>& sc,
                                                       int8_t compatibility);
 
+        Transaction& setFrameRateCategory(const sp<SurfaceControl>& sc, int8_t category);
+
         // Set by window manager indicating the layer and all its children are
         // in a different orientation than the display. The hint suggests that
         // the graphic producers should receive a transform hint as if the
diff --git a/libs/gui/include/gui/WindowInfo.h b/libs/gui/include/gui/WindowInfo.h
index 7ff7387..bd2eb74 100644
--- a/libs/gui/include/gui/WindowInfo.h
+++ b/libs/gui/include/gui/WindowInfo.h
@@ -194,10 +194,7 @@
     std::chrono::nanoseconds dispatchingTimeout = std::chrono::seconds(5);
 
     /* These values are filled in by SurfaceFlinger. */
-    int32_t frameLeft = -1;
-    int32_t frameTop = -1;
-    int32_t frameRight = -1;
-    int32_t frameBottom = -1;
+    Rect frame = Rect::INVALID_RECT;
 
     /*
      * SurfaceFlinger consumes this value to shrink the computed frame. This is
diff --git a/libs/gui/include/gui/bufferqueue/1.0/WGraphicBufferProducer.h b/libs/gui/include/gui/bufferqueue/1.0/WGraphicBufferProducer.h
index 004d875..32dc88b 100644
--- a/libs/gui/include/gui/bufferqueue/1.0/WGraphicBufferProducer.h
+++ b/libs/gui/include/gui/bufferqueue/1.0/WGraphicBufferProducer.h
@@ -298,7 +298,7 @@
     }
 
     Return<void> getConsumerName(HGraphicBufferProducer::getConsumerName_cb _hidl_cb) override {
-        _hidl_cb(mBase->getConsumerName().string());
+        _hidl_cb(mBase->getConsumerName().c_str());
         return Void();
     }
 
diff --git a/libs/gui/tests/GLTest.cpp b/libs/gui/tests/GLTest.cpp
index afeea42..40af8e8 100644
--- a/libs/gui/tests/GLTest.cpp
+++ b/libs/gui/tests/GLTest.cpp
@@ -177,31 +177,31 @@
         while ((err = glGetError()) != GL_NO_ERROR) {
             msg += String8::format(", %#x", err);
         }
-        return ::testing::AssertionFailure(::testing::Message(msg.string()));
+        return ::testing::AssertionFailure(::testing::Message(msg.c_str()));
     }
     if (r >= 0 && abs(r - int(pixel[0])) > tolerance) {
         msg += String8::format("r(%d isn't %d)", pixel[0], r);
     }
     if (g >= 0 && abs(g - int(pixel[1])) > tolerance) {
-        if (!msg.isEmpty()) {
+        if (!msg.empty()) {
             msg += " ";
         }
         msg += String8::format("g(%d isn't %d)", pixel[1], g);
     }
     if (b >= 0 && abs(b - int(pixel[2])) > tolerance) {
-        if (!msg.isEmpty()) {
+        if (!msg.empty()) {
             msg += " ";
         }
         msg += String8::format("b(%d isn't %d)", pixel[2], b);
     }
     if (a >= 0 && abs(a - int(pixel[3])) > tolerance) {
-        if (!msg.isEmpty()) {
+        if (!msg.empty()) {
             msg += " ";
         }
         msg += String8::format("a(%d isn't %d)", pixel[3], a);
     }
-    if (!msg.isEmpty()) {
-        return ::testing::AssertionFailure(::testing::Message(msg.string()));
+    if (!msg.empty()) {
+        return ::testing::AssertionFailure(::testing::Message(msg.c_str()));
     } else {
         return ::testing::AssertionSuccess();
     }
@@ -215,29 +215,29 @@
         msg += String8::format("left(%d isn't %d)", r1.left, r2.left);
     }
     if (abs(r1.top - r2.top) > tolerance) {
-        if (!msg.isEmpty()) {
+        if (!msg.empty()) {
             msg += " ";
         }
         msg += String8::format("top(%d isn't %d)", r1.top, r2.top);
     }
     if (abs(r1.right - r2.right) > tolerance) {
-        if (!msg.isEmpty()) {
+        if (!msg.empty()) {
             msg += " ";
         }
         msg += String8::format("right(%d isn't %d)", r1.right, r2.right);
     }
     if (abs(r1.bottom - r2.bottom) > tolerance) {
-        if (!msg.isEmpty()) {
+        if (!msg.empty()) {
             msg += " ";
         }
         msg += String8::format("bottom(%d isn't %d)", r1.bottom, r2.bottom);
     }
-    if (!msg.isEmpty()) {
+    if (!msg.empty()) {
         msg += String8::format(" R1: [%d %d %d %d] R2: [%d %d %d %d]",
                                r1.left, r1.top, r1.right, r1.bottom,
                                r2.left, r2.top, r2.right, r2.bottom);
-        fprintf(stderr, "assertRectEq: %s\n", msg.string());
-        return ::testing::AssertionFailure(::testing::Message(msg.string()));
+        fprintf(stderr, "assertRectEq: %s\n", msg.c_str());
+        return ::testing::AssertionFailure(::testing::Message(msg.c_str()));
     } else {
         return ::testing::AssertionSuccess();
     }
diff --git a/libs/gui/tests/SurfaceTextureFBO_test.cpp b/libs/gui/tests/SurfaceTextureFBO_test.cpp
index f34561f..ccd0e59 100644
--- a/libs/gui/tests/SurfaceTextureFBO_test.cpp
+++ b/libs/gui/tests/SurfaceTextureFBO_test.cpp
@@ -59,7 +59,7 @@
     glBindFramebuffer(GL_FRAMEBUFFER, 0);
 
     for (int i = 0; i < 4; i++) {
-        SCOPED_TRACE(String8::format("frame %d", i).string());
+        SCOPED_TRACE(String8::format("frame %d", i).c_str());
 
         ASSERT_EQ(NO_ERROR, native_window_dequeue_buffer_and_wait(mANW.get(),
                 &anb));
diff --git a/libs/gui/tests/SurfaceTextureGL_test.cpp b/libs/gui/tests/SurfaceTextureGL_test.cpp
index e2b4f3d..f76c0be 100644
--- a/libs/gui/tests/SurfaceTextureGL_test.cpp
+++ b/libs/gui/tests/SurfaceTextureGL_test.cpp
@@ -147,8 +147,9 @@
 
     for (int i = 0; i < 5; i++) {
         const android_native_rect_t& crop(crops[i]);
-        SCOPED_TRACE(String8::format("rect{ l: %d t: %d r: %d b: %d }",
-                crop.left, crop.top, crop.right, crop.bottom).string());
+        SCOPED_TRACE(String8::format("rect{ l: %d t: %d r: %d b: %d }", crop.left, crop.top,
+                                     crop.right, crop.bottom)
+                             .c_str());
 
         ASSERT_EQ(NO_ERROR, native_window_set_crop(mANW.get(), &crop));
 
@@ -308,7 +309,7 @@
     mFW->waitForFrame();
 
     for (int i = 0; i < numFrames; i++) {
-        SCOPED_TRACE(String8::format("frame %d", i).string());
+        SCOPED_TRACE(String8::format("frame %d", i).c_str());
 
         // We must wait for each frame to come in because if we ever do an
         // updateTexImage call that doesn't consume a newly available buffer
diff --git a/libs/gui/tests/Surface_test.cpp b/libs/gui/tests/Surface_test.cpp
index 30b7a9f..daed764 100644
--- a/libs/gui/tests/Surface_test.cpp
+++ b/libs/gui/tests/Surface_test.cpp
@@ -415,7 +415,7 @@
     sp<ANativeWindow> window(surface);
     native_window_api_connect(window.get(), NATIVE_WINDOW_API_CPU);
 
-    EXPECT_STREQ("TestConsumer", surface->getConsumerName().string());
+    EXPECT_STREQ("TestConsumer", surface->getConsumerName().c_str());
 }
 
 TEST_F(SurfaceTest, GetWideColorSupport) {
@@ -989,6 +989,25 @@
         return binder::Status::ok();
     }
 
+    binder::Status enableRefreshRateOverlay(bool /*active*/) override {
+        return binder::Status::ok();
+    }
+
+    binder::Status setDebugFlash(int /*delay*/) override { return binder::Status::ok(); }
+
+    binder::Status scheduleComposite() override { return binder::Status::ok(); }
+
+    binder::Status scheduleCommit() override { return binder::Status::ok(); }
+
+    binder::Status updateSmallAreaDetection(const std::vector<int32_t>& /*uids*/,
+                                            const std::vector<float>& /*thresholds*/) {
+        return binder::Status::ok();
+    }
+
+    binder::Status setSmallAreaDetectionThreshold(int32_t /*uid*/, float /*threshold*/) {
+        return binder::Status::ok();
+    }
+
     binder::Status getGpuContextPriority(int32_t* /*outPriority*/) override {
         return binder::Status::ok();
     }
diff --git a/libs/gui/tests/WindowInfo_test.cpp b/libs/gui/tests/WindowInfo_test.cpp
index 461fe4a..f2feaef 100644
--- a/libs/gui/tests/WindowInfo_test.cpp
+++ b/libs/gui/tests/WindowInfo_test.cpp
@@ -52,10 +52,7 @@
     i.layoutParamsFlags = WindowInfo::Flag::SLIPPERY;
     i.layoutParamsType = WindowInfo::Type::INPUT_METHOD;
     i.dispatchingTimeout = 12s;
-    i.frameLeft = 93;
-    i.frameTop = 34;
-    i.frameRight = 16;
-    i.frameBottom = 19;
+    i.frame = Rect(93, 34, 16, 19);
     i.surfaceInset = 17;
     i.globalScaleFactor = 0.3;
     i.alpha = 0.7;
@@ -85,10 +82,7 @@
     ASSERT_EQ(i.layoutParamsFlags, i2.layoutParamsFlags);
     ASSERT_EQ(i.layoutParamsType, i2.layoutParamsType);
     ASSERT_EQ(i.dispatchingTimeout, i2.dispatchingTimeout);
-    ASSERT_EQ(i.frameLeft, i2.frameLeft);
-    ASSERT_EQ(i.frameTop, i2.frameTop);
-    ASSERT_EQ(i.frameRight, i2.frameRight);
-    ASSERT_EQ(i.frameBottom, i2.frameBottom);
+    ASSERT_EQ(i.frame, i2.frame);
     ASSERT_EQ(i.surfaceInset, i2.surfaceInset);
     ASSERT_EQ(i.globalScaleFactor, i2.globalScaleFactor);
     ASSERT_EQ(i.alpha, i2.alpha);
diff --git a/libs/input/Android.bp b/libs/input/Android.bp
index 757cde2..8656b26 100644
--- a/libs/input/Android.bp
+++ b/libs/input/Android.bp
@@ -139,6 +139,7 @@
         "KeyCharacterMap.cpp",
         "KeyLayoutMap.cpp",
         "MotionPredictor.cpp",
+        "MotionPredictorMetricsManager.cpp",
         "PrintTools.cpp",
         "PropertyMap.cpp",
         "TfLiteMotionPredictor.cpp",
@@ -152,9 +153,13 @@
     header_libs: [
         "flatbuffer_headers",
         "jni_headers",
+        "libeigen",
         "tensorflow_headers",
     ],
-    export_header_lib_headers: ["jni_headers"],
+    export_header_lib_headers: [
+        "jni_headers",
+        "libeigen",
+    ],
 
     generated_headers: [
         "cxx-bridge-header",
@@ -206,6 +211,17 @@
 
     target: {
         android: {
+            export_shared_lib_headers: ["libbinder"],
+
+            shared_libs: [
+                "libutils",
+                "libbinder",
+                // Stats logging library and its dependencies.
+                "libstatslog_libinput",
+                "libstatsbootstrap",
+                "android.os.statsbootstrap_aidl-cpp",
+            ],
+
             required: [
                 "motion_predictor_model_prebuilt",
                 "motion_predictor_model_config",
@@ -228,6 +244,43 @@
     },
 }
 
+// Use bootstrap version of stats logging library.
+// libinput is a bootstrap process (starts early in the boot process), and thus can't use the normal
+// `libstatslog` because that requires `libstatssocket`, which is only available later in the boot.
+cc_library {
+    name: "libstatslog_libinput",
+    generated_sources: ["statslog_libinput.cpp"],
+    generated_headers: ["statslog_libinput.h"],
+    export_generated_headers: ["statslog_libinput.h"],
+    shared_libs: [
+        "libbinder",
+        "libstatsbootstrap",
+        "libutils",
+        "android.os.statsbootstrap_aidl-cpp",
+    ],
+}
+
+genrule {
+    name: "statslog_libinput.h",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --header $(genDir)/statslog_libinput.h --module libinput" +
+        " --namespace android,stats,libinput --bootstrap",
+    out: [
+        "statslog_libinput.h",
+    ],
+}
+
+genrule {
+    name: "statslog_libinput.cpp",
+    tools: ["stats-log-api-gen"],
+    cmd: "$(location stats-log-api-gen) --cpp $(genDir)/statslog_libinput.cpp --module libinput" +
+        " --namespace android,stats,libinput --importHeader statslog_libinput.h" +
+        " --bootstrap",
+    out: [
+        "statslog_libinput.cpp",
+    ],
+}
+
 cc_defaults {
     name: "libinput_fuzz_defaults",
     cpp_std: "c++20",
diff --git a/libs/input/InputTransport.cpp b/libs/input/InputTransport.cpp
index 3446540..5fec1a9 100644
--- a/libs/input/InputTransport.cpp
+++ b/libs/input/InputTransport.cpp
@@ -24,6 +24,7 @@
 #include <utils/Trace.h>
 
 #include <input/InputTransport.h>
+#include <input/TraceTools.h>
 
 namespace {
 
@@ -432,6 +433,10 @@
 }
 
 status_t InputChannel::sendMessage(const InputMessage* msg) {
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("sendMessage(inputChannel=%s, seq=0x%" PRIx32 ", type=0x%" PRIx32 ")",
+                            mName.c_str(), msg->header.seq, msg->header.type);
+    });
     const size_t msgLength = msg->size();
     InputMessage cleanMsg;
     msg->getSanitizedCopy(&cleanMsg);
@@ -463,16 +468,13 @@
     ALOGD_IF(DEBUG_CHANNEL_MESSAGES, "channel '%s' ~ sent message of type %s", mName.c_str(),
              ftl::enum_string(msg->header.type).c_str());
 
-    if (ATRACE_ENABLED()) {
-        std::string message =
-                StringPrintf("sendMessage(inputChannel=%s, seq=0x%" PRIx32 ", type=0x%" PRIx32 ")",
-                             mName.c_str(), msg->header.seq, msg->header.type);
-        ATRACE_NAME(message.c_str());
-    }
     return OK;
 }
 
 status_t InputChannel::receiveMessage(InputMessage* msg) {
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("receiveMessage(inputChannel=%s)", mName.c_str());
+    });
     ssize_t nRead;
     do {
         nRead = ::recv(getFd(), msg, sizeof(InputMessage), MSG_DONTWAIT);
@@ -504,8 +506,8 @@
 
     ALOGD_IF(DEBUG_CHANNEL_MESSAGES, "channel '%s' ~ received message of type %s", mName.c_str(),
              ftl::enum_string(msg->header.type).c_str());
-
     if (ATRACE_ENABLED()) {
+        // Add an additional trace point to include data about the received message.
         std::string message = StringPrintf("receiveMessage(inputChannel=%s, seq=0x%" PRIx32
                                            ", type=0x%" PRIx32 ")",
                                            mName.c_str(), msg->header.seq, msg->header.type);
@@ -578,13 +580,11 @@
                                          int32_t flags, int32_t keyCode, int32_t scanCode,
                                          int32_t metaState, int32_t repeatCount, nsecs_t downTime,
                                          nsecs_t eventTime) {
-    if (ATRACE_ENABLED()) {
-        std::string message =
-                StringPrintf("publishKeyEvent(inputChannel=%s, action=%s, keyCode=%s)",
-                             mChannel->getName().c_str(), KeyEvent::actionToString(action),
-                             KeyEvent::getLabel(keyCode));
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("publishKeyEvent(inputChannel=%s, action=%s, keyCode=%s)",
+                            mChannel->getName().c_str(), KeyEvent::actionToString(action),
+                            KeyEvent::getLabel(keyCode));
+    });
     ALOGD_IF(debugTransportPublisher(),
              "channel '%s' publisher ~ %s: seq=%u, id=%d, deviceId=%d, source=%s, "
              "action=%s, flags=0x%x, keyCode=%s, scanCode=%d, metaState=0x%x, repeatCount=%d,"
@@ -626,12 +626,11 @@
         const ui::Transform& rawTransform, nsecs_t downTime, nsecs_t eventTime,
         uint32_t pointerCount, const PointerProperties* pointerProperties,
         const PointerCoords* pointerCoords) {
-    if (ATRACE_ENABLED()) {
-        std::string message = StringPrintf("publishMotionEvent(inputChannel=%s, action=%s)",
-                                           mChannel->getName().c_str(),
-                                           MotionEvent::actionToString(action).c_str());
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("publishMotionEvent(inputChannel=%s, action=%s)",
+                            mChannel->getName().c_str(),
+                            MotionEvent::actionToString(action).c_str());
+    });
     if (verifyEvents()) {
         Result<void> result =
                 mInputVerifier.processMovement(deviceId, action, pointerCount, pointerProperties,
@@ -710,11 +709,10 @@
 }
 
 status_t InputPublisher::publishFocusEvent(uint32_t seq, int32_t eventId, bool hasFocus) {
-    if (ATRACE_ENABLED()) {
-        std::string message = StringPrintf("publishFocusEvent(inputChannel=%s, hasFocus=%s)",
-                                           mChannel->getName().c_str(), toString(hasFocus));
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("publishFocusEvent(inputChannel=%s, hasFocus=%s)",
+                            mChannel->getName().c_str(), toString(hasFocus));
+    });
     ALOGD_IF(debugTransportPublisher(), "channel '%s' publisher ~ %s: seq=%u, id=%d, hasFocus=%s",
              mChannel->getName().c_str(), __func__, seq, eventId, toString(hasFocus));
 
@@ -728,12 +726,10 @@
 
 status_t InputPublisher::publishCaptureEvent(uint32_t seq, int32_t eventId,
                                              bool pointerCaptureEnabled) {
-    if (ATRACE_ENABLED()) {
-        std::string message =
-                StringPrintf("publishCaptureEvent(inputChannel=%s, pointerCaptureEnabled=%s)",
-                             mChannel->getName().c_str(), toString(pointerCaptureEnabled));
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("publishCaptureEvent(inputChannel=%s, pointerCaptureEnabled=%s)",
+                            mChannel->getName().c_str(), toString(pointerCaptureEnabled));
+    });
     ALOGD_IF(debugTransportPublisher(),
              "channel '%s' publisher ~ %s: seq=%u, id=%d, pointerCaptureEnabled=%s",
              mChannel->getName().c_str(), __func__, seq, eventId, toString(pointerCaptureEnabled));
@@ -748,12 +744,10 @@
 
 status_t InputPublisher::publishDragEvent(uint32_t seq, int32_t eventId, float x, float y,
                                           bool isExiting) {
-    if (ATRACE_ENABLED()) {
-        std::string message =
-                StringPrintf("publishDragEvent(inputChannel=%s, x=%f, y=%f, isExiting=%s)",
-                             mChannel->getName().c_str(), x, y, toString(isExiting));
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("publishDragEvent(inputChannel=%s, x=%f, y=%f, isExiting=%s)",
+                            mChannel->getName().c_str(), x, y, toString(isExiting));
+    });
     ALOGD_IF(debugTransportPublisher(),
              "channel '%s' publisher ~ %s: seq=%u, id=%d, x=%f, y=%f, isExiting=%s",
              mChannel->getName().c_str(), __func__, seq, eventId, x, y, toString(isExiting));
@@ -769,12 +763,10 @@
 }
 
 status_t InputPublisher::publishTouchModeEvent(uint32_t seq, int32_t eventId, bool isInTouchMode) {
-    if (ATRACE_ENABLED()) {
-        std::string message =
-                StringPrintf("publishTouchModeEvent(inputChannel=%s, isInTouchMode=%s)",
-                             mChannel->getName().c_str(), toString(isInTouchMode));
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("publishTouchModeEvent(inputChannel=%s, isInTouchMode=%s)",
+                            mChannel->getName().c_str(), toString(isInTouchMode));
+    });
     ALOGD_IF(debugTransportPublisher(),
              "channel '%s' publisher ~ %s: seq=%u, id=%d, isInTouchMode=%s",
              mChannel->getName().c_str(), __func__, seq, eventId, toString(isInTouchMode));
@@ -791,8 +783,10 @@
     InputMessage msg;
     status_t result = mChannel->receiveMessage(&msg);
     if (result) {
-        ALOGD_IF(debugTransportPublisher(), "channel '%s' publisher ~ %s: %s",
-                 mChannel->getName().c_str(), __func__, strerror(result));
+        if (debugTransportPublisher() && result != WOULD_BLOCK) {
+            LOG(INFO) << "channel '" << mChannel->getName() << "' publisher ~ " << __func__ << ": "
+                      << strerror(result);
+        }
         return android::base::Error(result);
     }
     if (msg.header.type == InputMessage::Type::FINISHED) {
diff --git a/libs/input/KeyCharacterMap.cpp b/libs/input/KeyCharacterMap.cpp
index 12c9e53..a4cd239 100644
--- a/libs/input/KeyCharacterMap.cpp
+++ b/libs/input/KeyCharacterMap.cpp
@@ -140,7 +140,7 @@
 #if DEBUG_PARSER_PERFORMANCE
     nsecs_t elapsedTime = systemTime(SYSTEM_TIME_MONOTONIC) - startTime;
     ALOGD("Parsed key character map file '%s' %d lines in %0.3fms.",
-          tokenizer->getFilename().string(), tokenizer->getLineNumber(), elapsedTime / 1000000.0);
+          tokenizer->getFilename().c_str(), tokenizer->getLineNumber(), elapsedTime / 1000000.0);
 #endif
     if (status != OK) {
         ALOGE("Loading KeyCharacterMap failed with status %s", statusToString(status).c_str());
@@ -297,7 +297,7 @@
         if (!findKey(ch, &keyCode, &metaState)) {
 #if DEBUG_MAPPING
             ALOGD("getEvents: deviceId=%d, chars=[%s] ~ Failed to find mapping for character %d.",
-                    deviceId, toString(chars, numChars).string(), ch);
+                  deviceId, toString(chars, numChars).c_str(), ch);
 #endif
             return false;
         }
@@ -309,8 +309,8 @@
         addMetaKeys(outEvents, deviceId, metaState, false, now, &currentMetaState);
     }
 #if DEBUG_MAPPING
-    ALOGD("getEvents: deviceId=%d, chars=[%s] ~ Generated %d events.",
-            deviceId, toString(chars, numChars).string(), int32_t(outEvents.size()));
+    ALOGD("getEvents: deviceId=%d, chars=[%s] ~ Generated %d events.", deviceId,
+          toString(chars, numChars).c_str(), int32_t(outEvents.size()));
     for (size_t i = 0; i < outEvents.size(); i++) {
         ALOGD("  Key: keyCode=%d, metaState=0x%08x, %s.",
                 outEvents[i].getKeyCode(), outEvents[i].getMetaState(),
@@ -756,8 +756,8 @@
 status_t KeyCharacterMap::Parser::parse() {
     while (!mTokenizer->isEof()) {
 #if DEBUG_PARSER
-        ALOGD("Parsing %s: '%s'.", mTokenizer->getLocation().string(),
-                mTokenizer->peekRemainderOfLine().string());
+        ALOGD("Parsing %s: '%s'.", mTokenizer->getLocation().c_str(),
+              mTokenizer->peekRemainderOfLine().c_str());
 #endif
 
         mTokenizer->skipDelimiters(WHITESPACE);
@@ -779,8 +779,8 @@
                     status_t status = parseKey();
                     if (status) return status;
                 } else {
-                    ALOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().string(),
-                            keywordToken.string());
+                    ALOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().c_str(),
+                          keywordToken.c_str());
                     return BAD_VALUE;
                 }
                 break;
@@ -795,10 +795,9 @@
 
             mTokenizer->skipDelimiters(WHITESPACE);
             if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') {
-                ALOGE("%s: Expected end of line or trailing comment, got '%s'.",
-                        mTokenizer->getLocation().string(),
-                        mTokenizer->peekRemainderOfLine().string());
-                return BAD_VALUE;
+            ALOGE("%s: Expected end of line or trailing comment, got '%s'.",
+                  mTokenizer->getLocation().c_str(), mTokenizer->peekRemainderOfLine().c_str());
+            return BAD_VALUE;
             }
         }
 
@@ -807,27 +806,27 @@
 
     if (mState != STATE_TOP) {
         ALOGE("%s: Unterminated key description at end of file.",
-                mTokenizer->getLocation().string());
+              mTokenizer->getLocation().c_str());
         return BAD_VALUE;
     }
 
     if (mMap->mType == KeyboardType::UNKNOWN) {
         ALOGE("%s: Keyboard layout missing required keyboard 'type' declaration.",
-                mTokenizer->getLocation().string());
+              mTokenizer->getLocation().c_str());
         return BAD_VALUE;
     }
 
     if (mFormat == Format::BASE) {
         if (mMap->mType == KeyboardType::OVERLAY) {
             ALOGE("%s: Base keyboard layout must specify a keyboard 'type' other than 'OVERLAY'.",
-                    mTokenizer->getLocation().string());
+                  mTokenizer->getLocation().c_str());
             return BAD_VALUE;
         }
     } else if (mFormat == Format::OVERLAY) {
         if (mMap->mType != KeyboardType::OVERLAY) {
             ALOGE("%s: Overlay keyboard layout missing required keyboard "
-                    "'type OVERLAY' declaration.",
-                    mTokenizer->getLocation().string());
+                  "'type OVERLAY' declaration.",
+                  mTokenizer->getLocation().c_str());
             return BAD_VALUE;
         }
     }
@@ -837,8 +836,7 @@
 
 status_t KeyCharacterMap::Parser::parseType() {
     if (mMap->mType != KeyboardType::UNKNOWN) {
-        ALOGE("%s: Duplicate keyboard 'type' declaration.",
-                mTokenizer->getLocation().string());
+        ALOGE("%s: Duplicate keyboard 'type' declaration.", mTokenizer->getLocation().c_str());
         return BAD_VALUE;
     }
 
@@ -860,8 +858,8 @@
     } else if (typeToken == "OVERLAY") {
         type = KeyboardType::OVERLAY;
     } else {
-        ALOGE("%s: Expected keyboard type label, got '%s'.", mTokenizer->getLocation().string(),
-                typeToken.string());
+        ALOGE("%s: Expected keyboard type label, got '%s'.", mTokenizer->getLocation().c_str(),
+              typeToken.c_str());
         return BAD_VALUE;
     }
 
@@ -878,8 +876,8 @@
         mTokenizer->skipDelimiters(WHITESPACE);
         return parseMapKey();
     }
-    ALOGE("%s: Expected keyword after 'map', got '%s'.", mTokenizer->getLocation().string(),
-            keywordToken.string());
+    ALOGE("%s: Expected keyword after 'map', got '%s'.", mTokenizer->getLocation().c_str(),
+          keywordToken.c_str());
     return BAD_VALUE;
 }
 
@@ -893,26 +891,26 @@
     }
 
     char* end;
-    int32_t code = int32_t(strtol(codeToken.string(), &end, 0));
+    int32_t code = int32_t(strtol(codeToken.c_str(), &end, 0));
     if (*end) {
-        ALOGE("%s: Expected key %s number, got '%s'.", mTokenizer->getLocation().string(),
-                mapUsage ? "usage" : "scan code", codeToken.string());
+        ALOGE("%s: Expected key %s number, got '%s'.", mTokenizer->getLocation().c_str(),
+              mapUsage ? "usage" : "scan code", codeToken.c_str());
         return BAD_VALUE;
     }
     std::map<int32_t, int32_t>& map = mapUsage ? mMap->mKeysByUsageCode : mMap->mKeysByScanCode;
     const auto it = map.find(code);
     if (it != map.end()) {
-        ALOGE("%s: Duplicate entry for key %s '%s'.", mTokenizer->getLocation().string(),
-                mapUsage ? "usage" : "scan code", codeToken.string());
+        ALOGE("%s: Duplicate entry for key %s '%s'.", mTokenizer->getLocation().c_str(),
+                mapUsage ? "usage" : "scan code", codeToken.c_str());
         return BAD_VALUE;
     }
 
     mTokenizer->skipDelimiters(WHITESPACE);
     String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE);
-    std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.string());
+    std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.c_str());
     if (!keyCode) {
-        ALOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().string(),
-                keyCodeToken.string());
+        ALOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().c_str(),
+              keyCodeToken.c_str());
         return BAD_VALUE;
     }
 
@@ -926,23 +924,23 @@
 
 status_t KeyCharacterMap::Parser::parseKey() {
     String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE);
-    std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.string());
+    std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.c_str());
     if (!keyCode) {
-        ALOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().string(),
-                keyCodeToken.string());
+        ALOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().c_str(),
+              keyCodeToken.c_str());
         return BAD_VALUE;
     }
     if (mMap->mKeys.find(*keyCode) != mMap->mKeys.end()) {
-        ALOGE("%s: Duplicate entry for key code '%s'.", mTokenizer->getLocation().string(),
-                keyCodeToken.string());
+        ALOGE("%s: Duplicate entry for key code '%s'.", mTokenizer->getLocation().c_str(),
+                keyCodeToken.c_str());
         return BAD_VALUE;
     }
 
     mTokenizer->skipDelimiters(WHITESPACE);
     String8 openBraceToken = mTokenizer->nextToken(WHITESPACE);
     if (openBraceToken != "{") {
-        ALOGE("%s: Expected '{' after key code label, got '%s'.",
-                mTokenizer->getLocation().string(), openBraceToken.string());
+        ALOGE("%s: Expected '{' after key code label, got '%s'.", mTokenizer->getLocation().c_str(),
+              openBraceToken.c_str());
         return BAD_VALUE;
     }
 
@@ -971,10 +969,10 @@
             properties.emplace_back(PROPERTY_NUMBER);
         } else {
             int32_t metaState;
-            status_t status = parseModifier(token.string(), &metaState);
+            status_t status = parseModifier(token.c_str(), &metaState);
             if (status) {
                 ALOGE("%s: Expected a property name or modifier, got '%s'.",
-                        mTokenizer->getLocation().string(), token.string());
+                      mTokenizer->getLocation().c_str(), token.c_str());
                 return status;
             }
             properties.emplace_back(PROPERTY_META, metaState);
@@ -992,8 +990,7 @@
             }
         }
 
-        ALOGE("%s: Expected ',' or ':' after property name.",
-                mTokenizer->getLocation().string());
+        ALOGE("%s: Expected ',' or ':' after property name.", mTokenizer->getLocation().c_str());
         return BAD_VALUE;
     }
 
@@ -1011,18 +1008,17 @@
             char16_t character;
             status_t status = parseCharacterLiteral(&character);
             if (status || !character) {
-                ALOGE("%s: Invalid character literal for key.",
-                        mTokenizer->getLocation().string());
+                ALOGE("%s: Invalid character literal for key.", mTokenizer->getLocation().c_str());
                 return BAD_VALUE;
             }
             if (haveCharacter) {
                 ALOGE("%s: Cannot combine multiple character literals or 'none'.",
-                        mTokenizer->getLocation().string());
+                      mTokenizer->getLocation().c_str());
                 return BAD_VALUE;
             }
             if (haveReplacement) {
                 ALOGE("%s: Cannot combine character literal with replace action.",
-                        mTokenizer->getLocation().string());
+                      mTokenizer->getLocation().c_str());
                 return BAD_VALUE;
             }
             behavior.character = character;
@@ -1032,28 +1028,27 @@
             if (token == "none") {
                 if (haveCharacter) {
                     ALOGE("%s: Cannot combine multiple character literals or 'none'.",
-                            mTokenizer->getLocation().string());
+                          mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
                 }
                 if (haveReplacement) {
                     ALOGE("%s: Cannot combine 'none' with replace action.",
-                            mTokenizer->getLocation().string());
+                          mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
                 }
                 haveCharacter = true;
             } else if (token == "fallback") {
                 mTokenizer->skipDelimiters(WHITESPACE);
                 token = mTokenizer->nextToken(WHITESPACE);
-                std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(token.string());
+                std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(token.c_str());
                 if (!keyCode) {
                     ALOGE("%s: Invalid key code label for fallback behavior, got '%s'.",
-                            mTokenizer->getLocation().string(),
-                            token.string());
+                          mTokenizer->getLocation().c_str(), token.c_str());
                     return BAD_VALUE;
                 }
                 if (haveFallback || haveReplacement) {
                     ALOGE("%s: Cannot combine multiple fallback/replacement key codes.",
-                            mTokenizer->getLocation().string());
+                          mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
                 }
                 behavior.fallbackKeyCode = *keyCode;
@@ -1061,29 +1056,27 @@
             } else if (token == "replace") {
                 mTokenizer->skipDelimiters(WHITESPACE);
                 token = mTokenizer->nextToken(WHITESPACE);
-                std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(token.string());
+                std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(token.c_str());
                 if (!keyCode) {
                     ALOGE("%s: Invalid key code label for replace, got '%s'.",
-                            mTokenizer->getLocation().string(),
-                            token.string());
+                          mTokenizer->getLocation().c_str(), token.c_str());
                     return BAD_VALUE;
                 }
                 if (haveCharacter) {
                     ALOGE("%s: Cannot combine character literal with replace action.",
-                            mTokenizer->getLocation().string());
+                          mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
                 }
                 if (haveFallback || haveReplacement) {
                     ALOGE("%s: Cannot combine multiple fallback/replacement key codes.",
-                            mTokenizer->getLocation().string());
+                          mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
                 }
                 behavior.replacementKeyCode = *keyCode;
                 haveReplacement = true;
 
             } else {
-                ALOGE("%s: Expected a key behavior after ':'.",
-                        mTokenizer->getLocation().string());
+                ALOGE("%s: Expected a key behavior after ':'.", mTokenizer->getLocation().c_str());
                 return BAD_VALUE;
             }
         }
@@ -1096,7 +1089,7 @@
         switch (property.property) {
         case PROPERTY_LABEL:
                 if (key.label) {
-                    ALOGE("%s: Duplicate label for key.", mTokenizer->getLocation().string());
+                    ALOGE("%s: Duplicate label for key.", mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
                 }
                 key.label = behavior.character;
@@ -1106,7 +1099,7 @@
             break;
         case PROPERTY_NUMBER:
             if (key.number) {
-                    ALOGE("%s: Duplicate number for key.", mTokenizer->getLocation().string());
+                    ALOGE("%s: Duplicate number for key.", mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
             }
             key.number = behavior.character;
@@ -1118,7 +1111,7 @@
             for (const Behavior& b : key.behaviors) {
                     if (b.metaState == property.metaState) {
                     ALOGE("%s: Duplicate key behavior for modifier.",
-                            mTokenizer->getLocation().string());
+                          mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
                     }
             }
@@ -1185,8 +1178,8 @@
                 return BAD_VALUE;
             }
             if (combinedMeta & metaState) {
-                ALOGE("%s: Duplicate modifier combination '%s'.",
-                        mTokenizer->getLocation().string(), token.c_str());
+                ALOGE("%s: Duplicate modifier combination '%s'.", mTokenizer->getLocation().c_str(),
+                      token.c_str());
                 return BAD_VALUE;
             }
 
@@ -1254,12 +1247,12 @@
     }
 
     // Ensure that we consumed the entire token.
-    if (mTokenizer->nextToken(WHITESPACE).isEmpty()) {
+    if (mTokenizer->nextToken(WHITESPACE).empty()) {
         return NO_ERROR;
     }
 
 Error:
-    ALOGE("%s: Malformed character literal.", mTokenizer->getLocation().string());
+    ALOGE("%s: Malformed character literal.", mTokenizer->getLocation().c_str());
     return BAD_VALUE;
 }
 
diff --git a/libs/input/KeyLayoutMap.cpp b/libs/input/KeyLayoutMap.cpp
index a194513..ddc9ea4 100644
--- a/libs/input/KeyLayoutMap.cpp
+++ b/libs/input/KeyLayoutMap.cpp
@@ -177,7 +177,7 @@
 #if DEBUG_PARSER_PERFORMANCE
         nsecs_t elapsedTime = systemTime(SYSTEM_TIME_MONOTONIC) - startTime;
         ALOGD("Parsed key layout map file '%s' %d lines in %0.3fms.",
-              tokenizer->getFilename().string(), tokenizer->getLineNumber(),
+              tokenizer->getFilename().c_str(), tokenizer->getLineNumber(),
               elapsedTime / 1000000.0);
 #endif
         if (!status) {
@@ -306,8 +306,8 @@
 
 status_t KeyLayoutMap::Parser::parse() {
     while (!mTokenizer->isEof()) {
-        ALOGD_IF(DEBUG_PARSER, "Parsing %s: '%s'.", mTokenizer->getLocation().string(),
-                 mTokenizer->peekRemainderOfLine().string());
+        ALOGD_IF(DEBUG_PARSER, "Parsing %s: '%s'.", mTokenizer->getLocation().c_str(),
+                 mTokenizer->peekRemainderOfLine().c_str());
 
         mTokenizer->skipDelimiters(WHITESPACE);
 
@@ -334,16 +334,15 @@
                 status_t status = parseRequiredKernelConfig();
                 if (status) return status;
             } else {
-                ALOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().string(),
-                        keywordToken.string());
+                ALOGE("%s: Expected keyword, got '%s'.", mTokenizer->getLocation().c_str(),
+                      keywordToken.c_str());
                 return BAD_VALUE;
             }
 
             mTokenizer->skipDelimiters(WHITESPACE);
             if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') {
                 ALOGE("%s: Expected end of line or trailing comment, got '%s'.",
-                        mTokenizer->getLocation().string(),
-                        mTokenizer->peekRemainderOfLine().string());
+                      mTokenizer->getLocation().c_str(), mTokenizer->peekRemainderOfLine().c_str());
                 return BAD_VALUE;
             }
         }
@@ -362,26 +361,26 @@
         codeToken = mTokenizer->nextToken(WHITESPACE);
     }
 
-    std::optional<int> code = parseInt(codeToken.string());
+    std::optional<int> code = parseInt(codeToken.c_str());
     if (!code) {
-        ALOGE("%s: Expected key %s number, got '%s'.", mTokenizer->getLocation().string(),
-                mapUsage ? "usage" : "scan code", codeToken.string());
+        ALOGE("%s: Expected key %s number, got '%s'.", mTokenizer->getLocation().c_str(),
+                mapUsage ? "usage" : "scan code", codeToken.c_str());
         return BAD_VALUE;
     }
     std::unordered_map<int32_t, Key>& map =
             mapUsage ? mMap->mKeysByUsageCode : mMap->mKeysByScanCode;
     if (map.find(*code) != map.end()) {
-        ALOGE("%s: Duplicate entry for key %s '%s'.", mTokenizer->getLocation().string(),
-                mapUsage ? "usage" : "scan code", codeToken.string());
+        ALOGE("%s: Duplicate entry for key %s '%s'.", mTokenizer->getLocation().c_str(),
+                mapUsage ? "usage" : "scan code", codeToken.c_str());
         return BAD_VALUE;
     }
 
     mTokenizer->skipDelimiters(WHITESPACE);
     String8 keyCodeToken = mTokenizer->nextToken(WHITESPACE);
-    std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.string());
+    std::optional<int> keyCode = InputEventLookup::getKeyCodeByLabel(keyCodeToken.c_str());
     if (!keyCode) {
-        ALOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().string(),
-                keyCodeToken.string());
+        ALOGE("%s: Expected key code label, got '%s'.", mTokenizer->getLocation().c_str(),
+              keyCodeToken.c_str());
         return BAD_VALUE;
     }
 
@@ -391,15 +390,15 @@
         if (mTokenizer->isEol() || mTokenizer->peekChar() == '#') break;
 
         String8 flagToken = mTokenizer->nextToken(WHITESPACE);
-        std::optional<int> flag = InputEventLookup::getKeyFlagByLabel(flagToken.string());
+        std::optional<int> flag = InputEventLookup::getKeyFlagByLabel(flagToken.c_str());
         if (!flag) {
-            ALOGE("%s: Expected key flag label, got '%s'.", mTokenizer->getLocation().string(),
-                    flagToken.string());
+            ALOGE("%s: Expected key flag label, got '%s'.", mTokenizer->getLocation().c_str(),
+                  flagToken.c_str());
             return BAD_VALUE;
         }
         if (flags & *flag) {
-            ALOGE("%s: Duplicate key flag '%s'.", mTokenizer->getLocation().string(),
-                    flagToken.string());
+            ALOGE("%s: Duplicate key flag '%s'.", mTokenizer->getLocation().c_str(),
+                    flagToken.c_str());
             return BAD_VALUE;
         }
         flags |= *flag;
@@ -417,15 +416,15 @@
 
 status_t KeyLayoutMap::Parser::parseAxis() {
     String8 scanCodeToken = mTokenizer->nextToken(WHITESPACE);
-    std::optional<int> scanCode = parseInt(scanCodeToken.string());
+    std::optional<int> scanCode = parseInt(scanCodeToken.c_str());
     if (!scanCode) {
-        ALOGE("%s: Expected axis scan code number, got '%s'.", mTokenizer->getLocation().string(),
-                scanCodeToken.string());
+        ALOGE("%s: Expected axis scan code number, got '%s'.", mTokenizer->getLocation().c_str(),
+                scanCodeToken.c_str());
         return BAD_VALUE;
     }
     if (mMap->mAxes.find(*scanCode) != mMap->mAxes.end()) {
-        ALOGE("%s: Duplicate entry for axis scan code '%s'.", mTokenizer->getLocation().string(),
-                scanCodeToken.string());
+        ALOGE("%s: Duplicate entry for axis scan code '%s'.", mTokenizer->getLocation().c_str(),
+                scanCodeToken.c_str());
         return BAD_VALUE;
     }
 
@@ -438,10 +437,10 @@
 
         mTokenizer->skipDelimiters(WHITESPACE);
         String8 axisToken = mTokenizer->nextToken(WHITESPACE);
-        std::optional<int> axis = InputEventLookup::getAxisByLabel(axisToken.string());
+        std::optional<int> axis = InputEventLookup::getAxisByLabel(axisToken.c_str());
         if (!axis) {
             ALOGE("%s: Expected inverted axis label, got '%s'.",
-                    mTokenizer->getLocation().string(), axisToken.string());
+                    mTokenizer->getLocation().c_str(), axisToken.c_str());
             return BAD_VALUE;
         }
         axisInfo.axis = *axis;
@@ -450,38 +449,38 @@
 
         mTokenizer->skipDelimiters(WHITESPACE);
         String8 splitToken = mTokenizer->nextToken(WHITESPACE);
-        std::optional<int> splitValue = parseInt(splitToken.string());
+        std::optional<int> splitValue = parseInt(splitToken.c_str());
         if (!splitValue) {
             ALOGE("%s: Expected split value, got '%s'.",
-                    mTokenizer->getLocation().string(), splitToken.string());
+                    mTokenizer->getLocation().c_str(), splitToken.c_str());
             return BAD_VALUE;
         }
         axisInfo.splitValue = *splitValue;
 
         mTokenizer->skipDelimiters(WHITESPACE);
         String8 lowAxisToken = mTokenizer->nextToken(WHITESPACE);
-        std::optional<int> axis = InputEventLookup::getAxisByLabel(lowAxisToken.string());
+        std::optional<int> axis = InputEventLookup::getAxisByLabel(lowAxisToken.c_str());
         if (!axis) {
             ALOGE("%s: Expected low axis label, got '%s'.",
-                    mTokenizer->getLocation().string(), lowAxisToken.string());
+                    mTokenizer->getLocation().c_str(), lowAxisToken.c_str());
             return BAD_VALUE;
         }
         axisInfo.axis = *axis;
 
         mTokenizer->skipDelimiters(WHITESPACE);
         String8 highAxisToken = mTokenizer->nextToken(WHITESPACE);
-        std::optional<int> highAxis = InputEventLookup::getAxisByLabel(highAxisToken.string());
+        std::optional<int> highAxis = InputEventLookup::getAxisByLabel(highAxisToken.c_str());
         if (!highAxis) {
             ALOGE("%s: Expected high axis label, got '%s'.",
-                    mTokenizer->getLocation().string(), highAxisToken.string());
+                    mTokenizer->getLocation().c_str(), highAxisToken.c_str());
             return BAD_VALUE;
         }
         axisInfo.highAxis = *highAxis;
     } else {
-        std::optional<int> axis = InputEventLookup::getAxisByLabel(token.string());
+        std::optional<int> axis = InputEventLookup::getAxisByLabel(token.c_str());
         if (!axis) {
             ALOGE("%s: Expected axis label, 'split' or 'invert', got '%s'.",
-                    mTokenizer->getLocation().string(), token.string());
+                  mTokenizer->getLocation().c_str(), token.c_str());
             return BAD_VALUE;
         }
         axisInfo.axis = *axis;
@@ -496,16 +495,16 @@
         if (keywordToken == "flat") {
             mTokenizer->skipDelimiters(WHITESPACE);
             String8 flatToken = mTokenizer->nextToken(WHITESPACE);
-            std::optional<int> flatOverride = parseInt(flatToken.string());
+            std::optional<int> flatOverride = parseInt(flatToken.c_str());
             if (!flatOverride) {
                 ALOGE("%s: Expected flat value, got '%s'.",
-                        mTokenizer->getLocation().string(), flatToken.string());
+                        mTokenizer->getLocation().c_str(), flatToken.c_str());
                 return BAD_VALUE;
             }
             axisInfo.flatOverride = *flatOverride;
         } else {
-            ALOGE("%s: Expected keyword 'flat', got '%s'.",
-                    mTokenizer->getLocation().string(), keywordToken.string());
+            ALOGE("%s: Expected keyword 'flat', got '%s'.", mTokenizer->getLocation().c_str(),
+                  keywordToken.c_str());
             return BAD_VALUE;
         }
     }
@@ -527,27 +526,27 @@
         mTokenizer->skipDelimiters(WHITESPACE);
         codeToken = mTokenizer->nextToken(WHITESPACE);
     }
-    std::optional<int> code = parseInt(codeToken.string());
+    std::optional<int> code = parseInt(codeToken.c_str());
     if (!code) {
-        ALOGE("%s: Expected led %s number, got '%s'.", mTokenizer->getLocation().string(),
-                mapUsage ? "usage" : "scan code", codeToken.string());
+        ALOGE("%s: Expected led %s number, got '%s'.", mTokenizer->getLocation().c_str(),
+                mapUsage ? "usage" : "scan code", codeToken.c_str());
         return BAD_VALUE;
     }
 
     std::unordered_map<int32_t, Led>& map =
             mapUsage ? mMap->mLedsByUsageCode : mMap->mLedsByScanCode;
     if (map.find(*code) != map.end()) {
-        ALOGE("%s: Duplicate entry for led %s '%s'.", mTokenizer->getLocation().string(),
-                mapUsage ? "usage" : "scan code", codeToken.string());
+        ALOGE("%s: Duplicate entry for led %s '%s'.", mTokenizer->getLocation().c_str(),
+                mapUsage ? "usage" : "scan code", codeToken.c_str());
         return BAD_VALUE;
     }
 
     mTokenizer->skipDelimiters(WHITESPACE);
     String8 ledCodeToken = mTokenizer->nextToken(WHITESPACE);
-    std::optional<int> ledCode = InputEventLookup::getLedByLabel(ledCodeToken.string());
+    std::optional<int> ledCode = InputEventLookup::getLedByLabel(ledCodeToken.c_str());
     if (!ledCode) {
-        ALOGE("%s: Expected LED code label, got '%s'.", mTokenizer->getLocation().string(),
-                ledCodeToken.string());
+        ALOGE("%s: Expected LED code label, got '%s'.", mTokenizer->getLocation().c_str(),
+                ledCodeToken.c_str());
         return BAD_VALUE;
     }
 
@@ -569,7 +568,7 @@
 }
 
 static std::optional<int32_t> getSensorDataIndex(String8 token) {
-    std::string tokenStr(token.string());
+    std::string tokenStr(token.c_str());
     if (tokenStr == "X") {
         return 0;
     } else if (tokenStr == "Y") {
@@ -594,26 +593,26 @@
 // sensor 0x05 GYROSCOPE Z
 status_t KeyLayoutMap::Parser::parseSensor() {
     String8 codeToken = mTokenizer->nextToken(WHITESPACE);
-    std::optional<int> code = parseInt(codeToken.string());
+    std::optional<int> code = parseInt(codeToken.c_str());
     if (!code) {
-        ALOGE("%s: Expected sensor %s number, got '%s'.", mTokenizer->getLocation().string(),
-              "abs code", codeToken.string());
+        ALOGE("%s: Expected sensor %s number, got '%s'.", mTokenizer->getLocation().c_str(),
+              "abs code", codeToken.c_str());
         return BAD_VALUE;
     }
 
     std::unordered_map<int32_t, Sensor>& map = mMap->mSensorsByAbsCode;
     if (map.find(*code) != map.end()) {
-        ALOGE("%s: Duplicate entry for sensor %s '%s'.", mTokenizer->getLocation().string(),
-              "abs code", codeToken.string());
+        ALOGE("%s: Duplicate entry for sensor %s '%s'.", mTokenizer->getLocation().c_str(),
+              "abs code", codeToken.c_str());
         return BAD_VALUE;
     }
 
     mTokenizer->skipDelimiters(WHITESPACE);
     String8 sensorTypeToken = mTokenizer->nextToken(WHITESPACE);
-    std::optional<InputDeviceSensorType> typeOpt = getSensorType(sensorTypeToken.string());
+    std::optional<InputDeviceSensorType> typeOpt = getSensorType(sensorTypeToken.c_str());
     if (!typeOpt) {
-        ALOGE("%s: Expected sensor code label, got '%s'.", mTokenizer->getLocation().string(),
-              sensorTypeToken.string());
+        ALOGE("%s: Expected sensor code label, got '%s'.", mTokenizer->getLocation().c_str(),
+              sensorTypeToken.c_str());
         return BAD_VALUE;
     }
     InputDeviceSensorType sensorType = typeOpt.value();
@@ -621,8 +620,8 @@
     String8 sensorDataIndexToken = mTokenizer->nextToken(WHITESPACE);
     std::optional<int32_t> indexOpt = getSensorDataIndex(sensorDataIndexToken);
     if (!indexOpt) {
-        ALOGE("%s: Expected sensor data index label, got '%s'.", mTokenizer->getLocation().string(),
-              sensorDataIndexToken.string());
+        ALOGE("%s: Expected sensor data index label, got '%s'.", mTokenizer->getLocation().c_str(),
+              sensorDataIndexToken.c_str());
         return BAD_VALUE;
     }
     int32_t sensorDataIndex = indexOpt.value();
@@ -643,12 +642,12 @@
 // requires_kernel_config CONFIG_HID_PLAYSTATION
 status_t KeyLayoutMap::Parser::parseRequiredKernelConfig() {
     String8 codeToken = mTokenizer->nextToken(WHITESPACE);
-    std::string configName = codeToken.string();
+    std::string configName = codeToken.c_str();
 
     const auto result = mMap->mRequiredKernelConfigs.emplace(configName);
     if (!result.second) {
         ALOGE("%s: Duplicate entry for required kernel config %s.",
-              mTokenizer->getLocation().string(), configName.c_str());
+              mTokenizer->getLocation().c_str(), configName.c_str());
         return BAD_VALUE;
     }
 
diff --git a/libs/input/MotionPredictor.cpp b/libs/input/MotionPredictor.cpp
index 0961a9d..b5a5e72 100644
--- a/libs/input/MotionPredictor.cpp
+++ b/libs/input/MotionPredictor.cpp
@@ -137,10 +137,7 @@
 
     // Pass input event to the MetricsManager.
     if (!mMetricsManager) {
-        mMetricsManager =
-                std::make_optional<MotionPredictorMetricsManager>(mModel->config()
-                                                                          .predictionInterval,
-                                                                  mModel->outputLength());
+        mMetricsManager.emplace(mModel->config().predictionInterval, mModel->outputLength());
     }
     mMetricsManager->onRecord(event);
 
diff --git a/libs/input/MotionPredictorMetricsManager.cpp b/libs/input/MotionPredictorMetricsManager.cpp
new file mode 100644
index 0000000..67b1032
--- /dev/null
+++ b/libs/input/MotionPredictorMetricsManager.cpp
@@ -0,0 +1,373 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "MotionPredictorMetricsManager"
+
+#include <input/MotionPredictorMetricsManager.h>
+
+#include <algorithm>
+
+#include <android-base/logging.h>
+
+#include "Eigen/Core"
+#include "Eigen/Geometry"
+
+#ifdef __ANDROID__
+#include <statslog_libinput.h>
+#endif
+
+namespace android {
+namespace {
+
+inline constexpr int NANOS_PER_SECOND = 1'000'000'000; // nanoseconds per second
+inline constexpr int NANOS_PER_MILLIS = 1'000'000;     // nanoseconds per millisecond
+
+// Velocity threshold at which we report "high-velocity" metrics, in pixels per second.
+// This value was selected from manual experimentation, as a threshold that separates "fast"
+// (semi-sloppy) handwriting from more careful medium to slow handwriting.
+inline constexpr float HIGH_VELOCITY_THRESHOLD = 1100.0;
+
+// Small value to add to the path length when computing scale-invariant error to avoid division by
+// zero.
+inline constexpr float PATH_LENGTH_EPSILON = 0.001;
+
+} // namespace
+
+MotionPredictorMetricsManager::MotionPredictorMetricsManager(nsecs_t predictionInterval,
+                                                             size_t maxNumPredictions)
+      : mPredictionInterval(predictionInterval),
+        mMaxNumPredictions(maxNumPredictions),
+        mRecentGroundTruthPoints(maxNumPredictions + 1),
+        mAggregatedMetrics(maxNumPredictions),
+        mAtomFields(maxNumPredictions) {}
+
+void MotionPredictorMetricsManager::onRecord(const MotionEvent& inputEvent) {
+    // Convert MotionEvent to GroundTruthPoint.
+    const PointerCoords* coords = inputEvent.getRawPointerCoords(/*pointerIndex=*/0);
+    LOG_ALWAYS_FATAL_IF(coords == nullptr);
+    const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f{coords->getY(),
+                                                                         coords->getX()},
+                                             .pressure =
+                                                     inputEvent.getPressure(/*pointerIndex=*/0)},
+                                            .timestamp = inputEvent.getEventTime()};
+
+    // Handle event based on action type.
+    switch (inputEvent.getActionMasked()) {
+        case AMOTION_EVENT_ACTION_DOWN: {
+            clearStrokeData();
+            incorporateNewGroundTruth(groundTruthPoint);
+            break;
+        }
+        case AMOTION_EVENT_ACTION_MOVE: {
+            incorporateNewGroundTruth(groundTruthPoint);
+            break;
+        }
+        case AMOTION_EVENT_ACTION_UP:
+        case AMOTION_EVENT_ACTION_CANCEL: {
+            // Only expect meaningful predictions when given at least two input points.
+            if (mRecentGroundTruthPoints.size() >= 2) {
+                computeAtomFields();
+                reportMetrics();
+                break;
+            }
+        }
+    }
+}
+
+// Adds new predictions to mRecentPredictions and maintains the invariant that elements are
+// sorted in ascending order of targetTimestamp.
+void MotionPredictorMetricsManager::onPredict(const MotionEvent& predictionEvent) {
+    for (size_t i = 0; i < predictionEvent.getHistorySize() + 1; ++i) {
+        // Convert MotionEvent to PredictionPoint.
+        const PointerCoords* coords =
+                predictionEvent.getHistoricalRawPointerCoords(/*pointerIndex=*/0, i);
+        LOG_ALWAYS_FATAL_IF(coords == nullptr);
+        const nsecs_t targetTimestamp = predictionEvent.getHistoricalEventTime(i);
+        mRecentPredictions.push_back(
+                PredictionPoint{{.position = Eigen::Vector2f{coords->getY(), coords->getX()},
+                                 .pressure =
+                                         predictionEvent.getHistoricalPressure(/*pointerIndex=*/0,
+                                                                               i)},
+                                .originTimestamp = mRecentGroundTruthPoints.back().timestamp,
+                                .targetTimestamp = targetTimestamp});
+    }
+
+    std::sort(mRecentPredictions.begin(), mRecentPredictions.end());
+}
+
+void MotionPredictorMetricsManager::clearStrokeData() {
+    mRecentGroundTruthPoints.clear();
+    mRecentPredictions.clear();
+    std::fill(mAggregatedMetrics.begin(), mAggregatedMetrics.end(), AggregatedStrokeMetrics{});
+    std::fill(mAtomFields.begin(), mAtomFields.end(), AtomFields{});
+}
+
+void MotionPredictorMetricsManager::incorporateNewGroundTruth(
+        const GroundTruthPoint& groundTruthPoint) {
+    // Note: this removes the oldest point if `mRecentGroundTruthPoints` is already at capacity.
+    mRecentGroundTruthPoints.pushBack(groundTruthPoint);
+
+    // Remove outdated predictions – those that can never be matched with the current or any future
+    // ground truth points. We use fuzzy association for the timestamps here, because ground truth
+    // and prediction timestamps may not be perfectly synchronized.
+    const nsecs_t fuzzy_association_time_delta = mPredictionInterval / 4;
+    const auto firstCurrentIt =
+            std::find_if(mRecentPredictions.begin(), mRecentPredictions.end(),
+                         [&groundTruthPoint,
+                          fuzzy_association_time_delta](const PredictionPoint& prediction) {
+                             return prediction.targetTimestamp >
+                                     groundTruthPoint.timestamp - fuzzy_association_time_delta;
+                         });
+    mRecentPredictions.erase(mRecentPredictions.begin(), firstCurrentIt);
+
+    // Fuzzily match the new ground truth's timestamp to recent predictions' targetTimestamp and
+    // update the corresponding metrics.
+    for (const PredictionPoint& prediction : mRecentPredictions) {
+        if ((prediction.targetTimestamp >
+             groundTruthPoint.timestamp - fuzzy_association_time_delta) &&
+            (prediction.targetTimestamp <
+             groundTruthPoint.timestamp + fuzzy_association_time_delta)) {
+            updateAggregatedMetrics(prediction);
+        }
+    }
+}
+
+void MotionPredictorMetricsManager::updateAggregatedMetrics(
+        const PredictionPoint& predictionPoint) {
+    if (mRecentGroundTruthPoints.size() < 2) {
+        return;
+    }
+
+    const GroundTruthPoint& latestGroundTruthPoint = mRecentGroundTruthPoints.back();
+    const GroundTruthPoint& previousGroundTruthPoint =
+            mRecentGroundTruthPoints[mRecentGroundTruthPoints.size() - 2];
+    // Calculate prediction error vector.
+    const Eigen::Vector2f groundTruthTrajectory =
+            latestGroundTruthPoint.position - previousGroundTruthPoint.position;
+    const Eigen::Vector2f predictionTrajectory =
+            predictionPoint.position - previousGroundTruthPoint.position;
+    const Eigen::Vector2f predictionError = predictionTrajectory - groundTruthTrajectory;
+
+    // By default, prediction error counts fully as both off-trajectory and along-trajectory error.
+    // This serves as the fallback when the two most recent ground truth points are equal.
+    const float predictionErrorNorm = predictionError.norm();
+    float alongTrajectoryError = predictionErrorNorm;
+    float offTrajectoryError = predictionErrorNorm;
+    if (groundTruthTrajectory.squaredNorm() > 0) {
+        // Rotate the prediction error vector by the angle of the ground truth trajectory vector.
+        // This yields a vector whose first component is the along-trajectory error and whose
+        // second component is the off-trajectory error.
+        const float theta = std::atan2(groundTruthTrajectory[1], groundTruthTrajectory[0]);
+        const Eigen::Vector2f rotatedPredictionError = Eigen::Rotation2Df(-theta) * predictionError;
+        alongTrajectoryError = rotatedPredictionError[0];
+        offTrajectoryError = rotatedPredictionError[1];
+    }
+
+    // Compute the multiple of mPredictionInterval nearest to the amount of time into the
+    // future being predicted. This serves as the time bucket index into mAggregatedMetrics.
+    const float timestampDeltaFloat =
+            static_cast<float>(predictionPoint.targetTimestamp - predictionPoint.originTimestamp);
+    const size_t tIndex =
+            static_cast<size_t>(std::round(timestampDeltaFloat / mPredictionInterval - 1));
+
+    // Aggregate values into "general errors".
+    mAggregatedMetrics[tIndex].alongTrajectoryErrorSum += alongTrajectoryError;
+    mAggregatedMetrics[tIndex].alongTrajectorySumSquaredErrors +=
+            alongTrajectoryError * alongTrajectoryError;
+    mAggregatedMetrics[tIndex].offTrajectorySumSquaredErrors +=
+            offTrajectoryError * offTrajectoryError;
+    const float pressureError = predictionPoint.pressure - latestGroundTruthPoint.pressure;
+    mAggregatedMetrics[tIndex].pressureSumSquaredErrors += pressureError * pressureError;
+    ++mAggregatedMetrics[tIndex].generalErrorsCount;
+
+    // Aggregate values into high-velocity metrics, if we are in one of the last two time buckets
+    // and the velocity is above the threshold. Velocity here is measured in pixels per second.
+    const float velocity = groundTruthTrajectory.norm() /
+            (static_cast<float>(latestGroundTruthPoint.timestamp -
+                                previousGroundTruthPoint.timestamp) /
+             NANOS_PER_SECOND);
+    if ((tIndex + 2 >= mMaxNumPredictions) && (velocity > HIGH_VELOCITY_THRESHOLD)) {
+        mAggregatedMetrics[tIndex].highVelocityAlongTrajectorySse +=
+                alongTrajectoryError * alongTrajectoryError;
+        mAggregatedMetrics[tIndex].highVelocityOffTrajectorySse +=
+                offTrajectoryError * offTrajectoryError;
+        ++mAggregatedMetrics[tIndex].highVelocityErrorsCount;
+    }
+
+    // Compute path length for scale-invariant errors.
+    float pathLength = 0;
+    for (size_t i = 1; i < mRecentGroundTruthPoints.size(); ++i) {
+        pathLength +=
+                (mRecentGroundTruthPoints[i].position - mRecentGroundTruthPoints[i - 1].position)
+                        .norm();
+    }
+    // Avoid overweighting errors at the beginning of a stroke: compute the path length as if there
+    // were a full ground truth history by filling in missing segments with the average length.
+    // Note: the "- 1" is needed to translate from number of endpoints to number of segments.
+    pathLength *= static_cast<float>(mRecentGroundTruthPoints.capacity() - 1) /
+            (mRecentGroundTruthPoints.size() - 1);
+    pathLength += PATH_LENGTH_EPSILON; // Ensure path length is nonzero (>= PATH_LENGTH_EPSILON).
+
+    // Compute and aggregate scale-invariant errors.
+    const float scaleInvariantAlongTrajectoryError = alongTrajectoryError / pathLength;
+    const float scaleInvariantOffTrajectoryError = offTrajectoryError / pathLength;
+    mAggregatedMetrics[tIndex].scaleInvariantAlongTrajectorySse +=
+            scaleInvariantAlongTrajectoryError * scaleInvariantAlongTrajectoryError;
+    mAggregatedMetrics[tIndex].scaleInvariantOffTrajectorySse +=
+            scaleInvariantOffTrajectoryError * scaleInvariantOffTrajectoryError;
+    ++mAggregatedMetrics[tIndex].scaleInvariantErrorsCount;
+}
+
+void MotionPredictorMetricsManager::computeAtomFields() {
+    for (size_t i = 0; i < mAggregatedMetrics.size(); ++i) {
+        if (mAggregatedMetrics[i].generalErrorsCount == 0) {
+            // We have not received data corresponding to metrics for this time bucket.
+            continue;
+        }
+
+        mAtomFields[i].deltaTimeBucketMilliseconds =
+                static_cast<int>(mPredictionInterval / NANOS_PER_MILLIS * (i + 1));
+
+        // Note: we need the "* 1000"s below because we report values in integral milli-units.
+
+        { // General errors: reported for every time bucket.
+            const float alongTrajectoryErrorMean = mAggregatedMetrics[i].alongTrajectoryErrorSum /
+                    mAggregatedMetrics[i].generalErrorsCount;
+            mAtomFields[i].alongTrajectoryErrorMeanMillipixels =
+                    static_cast<int>(alongTrajectoryErrorMean * 1000);
+
+            const float alongTrajectoryMse = mAggregatedMetrics[i].alongTrajectorySumSquaredErrors /
+                    mAggregatedMetrics[i].generalErrorsCount;
+            // Take the max with 0 to avoid negative values caused by numerical instability.
+            const float alongTrajectoryErrorVariance =
+                    std::max(0.0f,
+                             alongTrajectoryMse -
+                                     alongTrajectoryErrorMean * alongTrajectoryErrorMean);
+            const float alongTrajectoryErrorStd = std::sqrt(alongTrajectoryErrorVariance);
+            mAtomFields[i].alongTrajectoryErrorStdMillipixels =
+                    static_cast<int>(alongTrajectoryErrorStd * 1000);
+
+            LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[i].offTrajectorySumSquaredErrors < 0,
+                                "mAggregatedMetrics[%zu].offTrajectorySumSquaredErrors = %f should "
+                                "not be negative",
+                                i, mAggregatedMetrics[i].offTrajectorySumSquaredErrors);
+            const float offTrajectoryRmse =
+                    std::sqrt(mAggregatedMetrics[i].offTrajectorySumSquaredErrors /
+                              mAggregatedMetrics[i].generalErrorsCount);
+            mAtomFields[i].offTrajectoryRmseMillipixels =
+                    static_cast<int>(offTrajectoryRmse * 1000);
+
+            LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[i].pressureSumSquaredErrors < 0,
+                                "mAggregatedMetrics[%zu].pressureSumSquaredErrors = %f should not "
+                                "be negative",
+                                i, mAggregatedMetrics[i].pressureSumSquaredErrors);
+            const float pressureRmse = std::sqrt(mAggregatedMetrics[i].pressureSumSquaredErrors /
+                                                 mAggregatedMetrics[i].generalErrorsCount);
+            mAtomFields[i].pressureRmseMilliunits = static_cast<int>(pressureRmse * 1000);
+        }
+
+        // High-velocity errors: reported only for last two time buckets.
+        // Check if we are in one of the last two time buckets, and there is high-velocity data.
+        if ((i + 2 >= mMaxNumPredictions) && (mAggregatedMetrics[i].highVelocityErrorsCount > 0)) {
+            LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[i].highVelocityAlongTrajectorySse < 0,
+                                "mAggregatedMetrics[%zu].highVelocityAlongTrajectorySse = %f "
+                                "should not be negative",
+                                i, mAggregatedMetrics[i].highVelocityAlongTrajectorySse);
+            const float alongTrajectoryRmse =
+                    std::sqrt(mAggregatedMetrics[i].highVelocityAlongTrajectorySse /
+                              mAggregatedMetrics[i].highVelocityErrorsCount);
+            mAtomFields[i].highVelocityAlongTrajectoryRmse =
+                    static_cast<int>(alongTrajectoryRmse * 1000);
+
+            LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[i].highVelocityOffTrajectorySse < 0,
+                                "mAggregatedMetrics[%zu].highVelocityOffTrajectorySse = %f should "
+                                "not be negative",
+                                i, mAggregatedMetrics[i].highVelocityOffTrajectorySse);
+            const float offTrajectoryRmse =
+                    std::sqrt(mAggregatedMetrics[i].highVelocityOffTrajectorySse /
+                              mAggregatedMetrics[i].highVelocityErrorsCount);
+            mAtomFields[i].highVelocityOffTrajectoryRmse =
+                    static_cast<int>(offTrajectoryRmse * 1000);
+        }
+
+        // Scale-invariant errors: reported only for the last time bucket, where the values
+        // represent an average across all time buckets.
+        if (i + 1 == mMaxNumPredictions) {
+            // Compute error averages.
+            float alongTrajectoryRmseSum = 0;
+            float offTrajectoryRmseSum = 0;
+            for (size_t j = 0; j < mAggregatedMetrics.size(); ++j) {
+                // If we have general errors (checked above), we should always also have
+                // scale-invariant errors.
+                LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[j].scaleInvariantErrorsCount == 0,
+                                    "mAggregatedMetrics[%zu].scaleInvariantErrorsCount is 0", j);
+
+                LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[j].scaleInvariantAlongTrajectorySse < 0,
+                                    "mAggregatedMetrics[%zu].scaleInvariantAlongTrajectorySse = %f "
+                                    "should not be negative",
+                                    j, mAggregatedMetrics[j].scaleInvariantAlongTrajectorySse);
+                alongTrajectoryRmseSum +=
+                        std::sqrt(mAggregatedMetrics[j].scaleInvariantAlongTrajectorySse /
+                                  mAggregatedMetrics[j].scaleInvariantErrorsCount);
+
+                LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[j].scaleInvariantOffTrajectorySse < 0,
+                                    "mAggregatedMetrics[%zu].scaleInvariantOffTrajectorySse = %f "
+                                    "should not be negative",
+                                    j, mAggregatedMetrics[j].scaleInvariantOffTrajectorySse);
+                offTrajectoryRmseSum +=
+                        std::sqrt(mAggregatedMetrics[j].scaleInvariantOffTrajectorySse /
+                                  mAggregatedMetrics[j].scaleInvariantErrorsCount);
+            }
+
+            const float averageAlongTrajectoryRmse =
+                    alongTrajectoryRmseSum / mAggregatedMetrics.size();
+            mAtomFields.back().scaleInvariantAlongTrajectoryRmse =
+                    static_cast<int>(averageAlongTrajectoryRmse * 1000);
+
+            const float averageOffTrajectoryRmse = offTrajectoryRmseSum / mAggregatedMetrics.size();
+            mAtomFields.back().scaleInvariantOffTrajectoryRmse =
+                    static_cast<int>(averageOffTrajectoryRmse * 1000);
+        }
+    }
+}
+
+void MotionPredictorMetricsManager::reportMetrics() {
+    // Report one atom for each time bucket.
+    for (size_t i = 0; i < mAtomFields.size(); ++i) {
+        // Call stats_write logging function only on Android targets (not supported on host).
+#ifdef __ANDROID__
+        android::stats::libinput::
+                stats_write(android::stats::libinput::STYLUS_PREDICTION_METRICS_REPORTED,
+                            /*stylus_vendor_id=*/0,
+                            /*stylus_product_id=*/0, mAtomFields[i].deltaTimeBucketMilliseconds,
+                            mAtomFields[i].alongTrajectoryErrorMeanMillipixels,
+                            mAtomFields[i].alongTrajectoryErrorStdMillipixels,
+                            mAtomFields[i].offTrajectoryRmseMillipixels,
+                            mAtomFields[i].pressureRmseMilliunits,
+                            mAtomFields[i].highVelocityAlongTrajectoryRmse,
+                            mAtomFields[i].highVelocityOffTrajectoryRmse,
+                            mAtomFields[i].scaleInvariantAlongTrajectoryRmse,
+                            mAtomFields[i].scaleInvariantOffTrajectoryRmse);
+#endif
+    }
+
+    // Set mock atom fields, if available.
+    if (mMockLoggedAtomFields != nullptr) {
+        *mMockLoggedAtomFields = mAtomFields;
+    }
+}
+
+} // namespace android
diff --git a/libs/input/PropertyMap.cpp b/libs/input/PropertyMap.cpp
index 548f894..5f6f9e2 100644
--- a/libs/input/PropertyMap.cpp
+++ b/libs/input/PropertyMap.cpp
@@ -163,16 +163,16 @@
 status_t PropertyMap::Parser::parse() {
     while (!mTokenizer->isEof()) {
 #if DEBUG_PARSER
-        ALOGD("Parsing %s: '%s'.", mTokenizer->getLocation().string(),
-              mTokenizer->peekRemainderOfLine().string());
+        ALOGD("Parsing %s: '%s'.", mTokenizer->getLocation().c_str(),
+              mTokenizer->peekRemainderOfLine().c_str());
 #endif
 
         mTokenizer->skipDelimiters(WHITESPACE);
 
         if (!mTokenizer->isEol() && mTokenizer->peekChar() != '#') {
             String8 keyToken = mTokenizer->nextToken(WHITESPACE_OR_PROPERTY_DELIMITER);
-            if (keyToken.isEmpty()) {
-                ALOGE("%s: Expected non-empty property key.", mTokenizer->getLocation().string());
+            if (keyToken.empty()) {
+                ALOGE("%s: Expected non-empty property key.", mTokenizer->getLocation().c_str());
                 return BAD_VALUE;
             }
 
@@ -180,7 +180,7 @@
 
             if (mTokenizer->nextChar() != '=') {
                 ALOGE("%s: Expected '=' between property key and value.",
-                      mTokenizer->getLocation().string());
+                      mTokenizer->getLocation().c_str());
                 return BAD_VALUE;
             }
 
@@ -189,24 +189,24 @@
             String8 valueToken = mTokenizer->nextToken(WHITESPACE);
             if (valueToken.find("\\", 0) >= 0 || valueToken.find("\"", 0) >= 0) {
                 ALOGE("%s: Found reserved character '\\' or '\"' in property value.",
-                      mTokenizer->getLocation().string());
+                      mTokenizer->getLocation().c_str());
                 return BAD_VALUE;
             }
 
             mTokenizer->skipDelimiters(WHITESPACE);
             if (!mTokenizer->isEol()) {
-                ALOGE("%s: Expected end of line, got '%s'.", mTokenizer->getLocation().string(),
-                      mTokenizer->peekRemainderOfLine().string());
+                ALOGE("%s: Expected end of line, got '%s'.", mTokenizer->getLocation().c_str(),
+                      mTokenizer->peekRemainderOfLine().c_str());
                 return BAD_VALUE;
             }
 
-            if (mMap->hasProperty(keyToken.string())) {
+            if (mMap->hasProperty(keyToken.c_str())) {
                 ALOGE("%s: Duplicate property value for key '%s'.",
-                      mTokenizer->getLocation().string(), keyToken.string());
+                      mTokenizer->getLocation().c_str(), keyToken.c_str());
                 return BAD_VALUE;
             }
 
-            mMap->addProperty(keyToken.string(), valueToken.string());
+            mMap->addProperty(keyToken.c_str(), valueToken.c_str());
         }
 
         mTokenizer->nextLine();
diff --git a/libs/input/VirtualKeyMap.cpp b/libs/input/VirtualKeyMap.cpp
index 865366b..8b8af42 100644
--- a/libs/input/VirtualKeyMap.cpp
+++ b/libs/input/VirtualKeyMap.cpp
@@ -79,8 +79,8 @@
 status_t VirtualKeyMap::Parser::parse() {
     while (!mTokenizer->isEof()) {
 #if DEBUG_PARSER
-        ALOGD("Parsing %s: '%s'.", mTokenizer->getLocation().string(),
-                mTokenizer->peekRemainderOfLine().string());
+        ALOGD("Parsing %s: '%s'.", mTokenizer->getLocation().c_str(),
+              mTokenizer->peekRemainderOfLine().c_str());
 #endif
 
         mTokenizer->skipDelimiters(WHITESPACE);
@@ -91,7 +91,7 @@
                 String8 token = mTokenizer->nextToken(WHITESPACE_OR_FIELD_DELIMITER);
                 if (token != "0x01") {
                     ALOGE("%s: Unknown virtual key type, expected 0x01.",
-                          mTokenizer->getLocation().string());
+                          mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
                 }
 
@@ -103,7 +103,7 @@
                         && parseNextIntField(&defn.height);
                 if (!success) {
                     ALOGE("%s: Expected 5 colon-delimited integers in virtual key definition.",
-                          mTokenizer->getLocation().string());
+                          mTokenizer->getLocation().c_str());
                     return BAD_VALUE;
                 }
 
@@ -116,9 +116,8 @@
             } while (consumeFieldDelimiterAndSkipWhitespace());
 
             if (!mTokenizer->isEol()) {
-                ALOGE("%s: Expected end of line, got '%s'.",
-                        mTokenizer->getLocation().string(),
-                        mTokenizer->peekRemainderOfLine().string());
+                ALOGE("%s: Expected end of line, got '%s'.", mTokenizer->getLocation().c_str(),
+                      mTokenizer->peekRemainderOfLine().c_str());
                 return BAD_VALUE;
             }
         }
@@ -146,9 +145,9 @@
 
     String8 token = mTokenizer->nextToken(WHITESPACE_OR_FIELD_DELIMITER);
     char* end;
-    *outValue = strtol(token.string(), &end, 0);
-    if (token.isEmpty() || *end != '\0') {
-        ALOGE("Expected an integer, got '%s'.", token.string());
+    *outValue = strtol(token.c_str(), &end, 0);
+    if (token.empty() || *end != '\0') {
+        ALOGE("Expected an integer, got '%s'.", token.c_str());
         return false;
     }
     return true;
diff --git a/libs/input/tests/Android.bp b/libs/input/tests/Android.bp
index 86b996b..e7224ff 100644
--- a/libs/input/tests/Android.bp
+++ b/libs/input/tests/Android.bp
@@ -20,6 +20,7 @@
         "InputPublisherAndConsumer_test.cpp",
         "InputVerifier_test.cpp",
         "MotionPredictor_test.cpp",
+        "MotionPredictorMetricsManager_test.cpp",
         "RingBuffer_test.cpp",
         "TfLiteMotionPredictor_test.cpp",
         "TouchResampling_test.cpp",
@@ -52,13 +53,6 @@
             undefined: true,
         },
     },
-    target: {
-        host: {
-            sanitize: {
-                address: true,
-            },
-        },
-    },
     shared_libs: [
         "libbase",
         "libbinder",
@@ -77,6 +71,21 @@
         unit_test: true,
     },
     test_suites: ["device-tests"],
+    target: {
+        host: {
+            sanitize: {
+                address: true,
+            },
+        },
+        android: {
+            static_libs: [
+                // Stats logging library and its dependencies.
+                "libstatslog_libinput",
+                "libstatsbootstrap",
+                "android.os.statsbootstrap_aidl-cpp",
+            ],
+        },
+    },
 }
 
 // NOTE: This is a compile time test, and does not need to be
diff --git a/libs/input/tests/MotionPredictorMetricsManager_test.cpp b/libs/input/tests/MotionPredictorMetricsManager_test.cpp
new file mode 100644
index 0000000..b420a5a
--- /dev/null
+++ b/libs/input/tests/MotionPredictorMetricsManager_test.cpp
@@ -0,0 +1,972 @@
+/*
+ * 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 <input/MotionPredictor.h>
+
+#include <cmath>
+#include <cstddef>
+#include <cstdint>
+#include <numeric>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <input/InputEventBuilders.h>
+#include <utils/Timers.h> // for nsecs_t
+
+#include "Eigen/Core"
+#include "Eigen/Geometry"
+
+namespace android {
+namespace {
+
+using ::testing::FloatNear;
+using ::testing::Matches;
+
+using GroundTruthPoint = MotionPredictorMetricsManager::GroundTruthPoint;
+using PredictionPoint = MotionPredictorMetricsManager::PredictionPoint;
+using AtomFields = MotionPredictorMetricsManager::AtomFields;
+
+inline constexpr int NANOS_PER_MILLIS = 1'000'000;
+
+inline constexpr nsecs_t TEST_INITIAL_TIMESTAMP = 1'000'000'000;
+inline constexpr size_t TEST_MAX_NUM_PREDICTIONS = 5;
+inline constexpr nsecs_t TEST_PREDICTION_INTERVAL_NANOS = 12'500'000 / 3; // 1 / (240 hz)
+inline constexpr int NO_DATA_SENTINEL = MotionPredictorMetricsManager::NO_DATA_SENTINEL;
+
+// Parameters:
+//  • arg: Eigen::Vector2f
+//  • target: Eigen::Vector2f
+//  • epsilon: float
+MATCHER_P2(Vector2fNear, target, epsilon, "") {
+    return Matches(FloatNear(target[0], epsilon))(arg[0]) &&
+            Matches(FloatNear(target[1], epsilon))(arg[1]);
+}
+
+// Parameters:
+//  • arg: PredictionPoint
+//  • target: PredictionPoint
+//  • epsilon: float
+MATCHER_P2(PredictionPointNear, target, epsilon, "") {
+    if (!Matches(Vector2fNear(target.position, epsilon))(arg.position)) {
+        *result_listener << "Position mismatch. Actual: (" << arg.position[0] << ", "
+                         << arg.position[1] << "), expected: (" << target.position[0] << ", "
+                         << target.position[1] << ")";
+        return false;
+    }
+    if (!Matches(FloatNear(target.pressure, epsilon))(arg.pressure)) {
+        *result_listener << "Pressure mismatch. Actual: " << arg.pressure
+                         << ", expected: " << target.pressure;
+        return false;
+    }
+    if (arg.originTimestamp != target.originTimestamp) {
+        *result_listener << "Origin timestamp mismatch. Actual: " << arg.originTimestamp
+                         << ", expected: " << target.originTimestamp;
+        return false;
+    }
+    if (arg.targetTimestamp != target.targetTimestamp) {
+        *result_listener << "Target timestamp mismatch. Actual: " << arg.targetTimestamp
+                         << ", expected: " << target.targetTimestamp;
+        return false;
+    }
+    return true;
+}
+
+// --- Mathematical helper functions. ---
+
+template <typename T>
+T average(std::vector<T> values) {
+    return std::accumulate(values.begin(), values.end(), T{}) / static_cast<T>(values.size());
+}
+
+template <typename T>
+T standardDeviation(std::vector<T> values) {
+    T mean = average(values);
+    T accumulator = {};
+    for (const T value : values) {
+        accumulator += value * value - mean * mean;
+    }
+    // Take the max with 0 to avoid negative values caused by numerical instability.
+    return std::sqrt(std::max(T{}, accumulator) / static_cast<T>(values.size()));
+}
+
+template <typename T>
+T rmse(std::vector<T> errors) {
+    T sse = {};
+    for (const T error : errors) {
+        sse += error * error;
+    }
+    return std::sqrt(sse / static_cast<T>(errors.size()));
+}
+
+TEST(MathematicalHelperFunctionTest, Average) {
+    std::vector<float> values{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+    EXPECT_EQ(5.5f, average(values));
+}
+
+TEST(MathematicalHelperFunctionTest, StandardDeviation) {
+    // https://www.calculator.net/standard-deviation-calculator.html?numberinputs=10%2C+12%2C+23%2C+23%2C+16%2C+23%2C+21%2C+16
+    std::vector<float> values{10, 12, 23, 23, 16, 23, 21, 16};
+    EXPECT_FLOAT_EQ(4.8989794855664f, standardDeviation(values));
+}
+
+TEST(MathematicalHelperFunctionTest, Rmse) {
+    std::vector<float> errors{1, 5, 7, 7, 8, 20};
+    EXPECT_FLOAT_EQ(9.899494937f, rmse(errors));
+}
+
+// --- MotionEvent-related helper functions. ---
+
+// Creates a MotionEvent corresponding to the given GroundTruthPoint.
+MotionEvent makeMotionEvent(const GroundTruthPoint& groundTruthPoint) {
+    // Build single pointer of type STYLUS, with coordinates from groundTruthPoint.
+    PointerBuilder pointerBuilder =
+            PointerBuilder(/*id=*/0, ToolType::STYLUS)
+                    .x(groundTruthPoint.position[1])
+                    .y(groundTruthPoint.position[0])
+                    .axis(AMOTION_EVENT_AXIS_PRESSURE, groundTruthPoint.pressure);
+    return MotionEventBuilder(/*action=*/AMOTION_EVENT_ACTION_MOVE,
+                              /*source=*/AINPUT_SOURCE_CLASS_POINTER)
+            .eventTime(groundTruthPoint.timestamp)
+            .pointer(pointerBuilder)
+            .build();
+}
+
+// Creates a MotionEvent corresponding to the given sequence of PredictionPoints.
+MotionEvent makeMotionEvent(const std::vector<PredictionPoint>& predictionPoints) {
+    // Build single pointer of type STYLUS, with coordinates from first prediction point.
+    PointerBuilder pointerBuilder =
+            PointerBuilder(/*id=*/0, ToolType::STYLUS)
+                    .x(predictionPoints[0].position[1])
+                    .y(predictionPoints[0].position[0])
+                    .axis(AMOTION_EVENT_AXIS_PRESSURE, predictionPoints[0].pressure);
+    MotionEvent predictionEvent =
+            MotionEventBuilder(
+                    /*action=*/AMOTION_EVENT_ACTION_MOVE, /*source=*/AINPUT_SOURCE_CLASS_POINTER)
+                    .eventTime(predictionPoints[0].targetTimestamp)
+                    .pointer(pointerBuilder)
+                    .build();
+    for (size_t i = 1; i < predictionPoints.size(); ++i) {
+        PointerCoords coords =
+                PointerBuilder(/*id=*/0, ToolType::STYLUS)
+                        .x(predictionPoints[i].position[1])
+                        .y(predictionPoints[i].position[0])
+                        .axis(AMOTION_EVENT_AXIS_PRESSURE, predictionPoints[i].pressure)
+                        .buildCoords();
+        predictionEvent.addSample(predictionPoints[i].targetTimestamp, &coords);
+    }
+    return predictionEvent;
+}
+
+// Creates a MotionEvent corresponding to a stylus lift (UP) ground truth event.
+MotionEvent makeLiftMotionEvent() {
+    return MotionEventBuilder(/*action=*/AMOTION_EVENT_ACTION_UP,
+                              /*source=*/AINPUT_SOURCE_CLASS_POINTER)
+            .pointer(PointerBuilder(/*id=*/0, ToolType::STYLUS))
+            .build();
+}
+
+TEST(MakeMotionEventTest, MakeGroundTruthMotionEvent) {
+    const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10.0f, 20.0f),
+                                             .pressure = 0.6f},
+                                            .timestamp = TEST_INITIAL_TIMESTAMP};
+    const MotionEvent groundTruthMotionEvent = makeMotionEvent(groundTruthPoint);
+
+    ASSERT_EQ(1u, groundTruthMotionEvent.getPointerCount());
+    // Note: a MotionEvent's "history size" is one less than its number of samples.
+    ASSERT_EQ(0u, groundTruthMotionEvent.getHistorySize());
+    EXPECT_EQ(groundTruthPoint.position[0], groundTruthMotionEvent.getRawPointerCoords(0)->getY());
+    EXPECT_EQ(groundTruthPoint.position[1], groundTruthMotionEvent.getRawPointerCoords(0)->getX());
+    EXPECT_EQ(groundTruthPoint.pressure,
+              groundTruthMotionEvent.getRawPointerCoords(0)->getAxisValue(
+                      AMOTION_EVENT_AXIS_PRESSURE));
+    EXPECT_EQ(AMOTION_EVENT_ACTION_MOVE, groundTruthMotionEvent.getAction());
+}
+
+TEST(MakeMotionEventTest, MakePredictionMotionEvent) {
+    const nsecs_t originTimestamp = TEST_INITIAL_TIMESTAMP;
+    const std::vector<PredictionPoint>
+            predictionPoints{{{.position = Eigen::Vector2f(10.0f, 20.0f), .pressure = 0.6f},
+                              .originTimestamp = originTimestamp,
+                              .targetTimestamp = originTimestamp + 5 * NANOS_PER_MILLIS},
+                             {{.position = Eigen::Vector2f(11.0f, 22.0f), .pressure = 0.5f},
+                              .originTimestamp = originTimestamp,
+                              .targetTimestamp = originTimestamp + 10 * NANOS_PER_MILLIS},
+                             {{.position = Eigen::Vector2f(12.0f, 24.0f), .pressure = 0.4f},
+                              .originTimestamp = originTimestamp,
+                              .targetTimestamp = originTimestamp + 15 * NANOS_PER_MILLIS}};
+    const MotionEvent predictionMotionEvent = makeMotionEvent(predictionPoints);
+
+    ASSERT_EQ(1u, predictionMotionEvent.getPointerCount());
+    // Note: a MotionEvent's "history size" is one less than its number of samples.
+    ASSERT_EQ(predictionPoints.size(), predictionMotionEvent.getHistorySize() + 1);
+    for (size_t i = 0; i < predictionPoints.size(); ++i) {
+        SCOPED_TRACE(testing::Message() << "i = " << i);
+        const PointerCoords coords = *predictionMotionEvent.getHistoricalRawPointerCoords(
+                /*pointerIndex=*/0, /*historicalIndex=*/i);
+        EXPECT_EQ(predictionPoints[i].position[0], coords.getY());
+        EXPECT_EQ(predictionPoints[i].position[1], coords.getX());
+        EXPECT_EQ(predictionPoints[i].pressure, coords.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE));
+        // Note: originTimestamp is discarded when converting PredictionPoint to MotionEvent.
+        EXPECT_EQ(predictionPoints[i].targetTimestamp,
+                  predictionMotionEvent.getHistoricalEventTime(i));
+        EXPECT_EQ(AMOTION_EVENT_ACTION_MOVE, predictionMotionEvent.getAction());
+    }
+}
+
+TEST(MakeMotionEventTest, MakeLiftMotionEvent) {
+    const MotionEvent liftMotionEvent = makeLiftMotionEvent();
+    ASSERT_EQ(1u, liftMotionEvent.getPointerCount());
+    // Note: a MotionEvent's "history size" is one less than its number of samples.
+    ASSERT_EQ(0u, liftMotionEvent.getHistorySize());
+    EXPECT_EQ(AMOTION_EVENT_ACTION_UP, liftMotionEvent.getAction());
+}
+
+// --- Ground-truth-generation helper functions. ---
+
+std::vector<GroundTruthPoint> generateConstantGroundTruthPoints(
+        const GroundTruthPoint& groundTruthPoint, size_t numPoints) {
+    std::vector<GroundTruthPoint> groundTruthPoints;
+    nsecs_t timestamp = groundTruthPoint.timestamp;
+    for (size_t i = 0; i < numPoints; ++i) {
+        groundTruthPoints.emplace_back(groundTruthPoint);
+        groundTruthPoints.back().timestamp = timestamp;
+        timestamp += TEST_PREDICTION_INTERVAL_NANOS;
+    }
+    return groundTruthPoints;
+}
+
+// This function uses the coordinate system (y, x), with +y pointing downwards and +x pointing
+// rightwards. Angles are measured counterclockwise from down (+y).
+std::vector<GroundTruthPoint> generateCircularArcGroundTruthPoints(Eigen::Vector2f initialPosition,
+                                                                   float initialAngle,
+                                                                   float velocity,
+                                                                   float turningAngle,
+                                                                   size_t numPoints) {
+    std::vector<GroundTruthPoint> groundTruthPoints;
+    // Create first point.
+    if (numPoints > 0) {
+        groundTruthPoints.push_back({{.position = initialPosition, .pressure = 0.0f},
+                                     .timestamp = TEST_INITIAL_TIMESTAMP});
+    }
+    float trajectoryAngle = initialAngle; // measured counterclockwise from +y axis.
+    for (size_t i = 1; i < numPoints; ++i) {
+        const Eigen::Vector2f trajectory =
+                Eigen::Rotation2D(trajectoryAngle) * Eigen::Vector2f(1, 0);
+        groundTruthPoints.push_back(
+                {{.position = groundTruthPoints.back().position + velocity * trajectory,
+                  .pressure = 0.0f},
+                 .timestamp = groundTruthPoints.back().timestamp + TEST_PREDICTION_INTERVAL_NANOS});
+        trajectoryAngle += turningAngle;
+    }
+    return groundTruthPoints;
+}
+
+TEST(GenerateConstantGroundTruthPointsTest, BasicTest) {
+    const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10, 20), .pressure = 0.3f},
+                                            .timestamp = TEST_INITIAL_TIMESTAMP};
+    const std::vector<GroundTruthPoint> groundTruthPoints =
+            generateConstantGroundTruthPoints(groundTruthPoint, /*numPoints=*/3);
+
+    ASSERT_EQ(3u, groundTruthPoints.size());
+    // First point.
+    EXPECT_EQ(groundTruthPoints[0].position, groundTruthPoint.position);
+    EXPECT_EQ(groundTruthPoints[0].pressure, groundTruthPoint.pressure);
+    EXPECT_EQ(groundTruthPoints[0].timestamp, groundTruthPoint.timestamp);
+    // Second point.
+    EXPECT_EQ(groundTruthPoints[1].position, groundTruthPoint.position);
+    EXPECT_EQ(groundTruthPoints[1].pressure, groundTruthPoint.pressure);
+    EXPECT_GT(groundTruthPoints[1].timestamp, groundTruthPoints[0].timestamp);
+    // Third point.
+    EXPECT_EQ(groundTruthPoints[2].position, groundTruthPoint.position);
+    EXPECT_EQ(groundTruthPoints[2].pressure, groundTruthPoint.pressure);
+    EXPECT_GT(groundTruthPoints[2].timestamp, groundTruthPoints[1].timestamp);
+}
+
+TEST(GenerateCircularArcGroundTruthTest, StraightLineUpwards) {
+    const std::vector<GroundTruthPoint> groundTruthPoints = generateCircularArcGroundTruthPoints(
+            /*initialPosition=*/Eigen::Vector2f(0, 0),
+            /*initialAngle=*/M_PI,
+            /*velocity=*/1.0f,
+            /*turningAngle=*/0.0f,
+            /*numPoints=*/3);
+
+    ASSERT_EQ(3u, groundTruthPoints.size());
+    EXPECT_THAT(groundTruthPoints[0].position, Vector2fNear(Eigen::Vector2f(0, 0), 1e-6));
+    EXPECT_THAT(groundTruthPoints[1].position, Vector2fNear(Eigen::Vector2f(-1, 0), 1e-6));
+    EXPECT_THAT(groundTruthPoints[2].position, Vector2fNear(Eigen::Vector2f(-2, 0), 1e-6));
+    // Check that timestamps are increasing between consecutive ground truth points.
+    EXPECT_GT(groundTruthPoints[1].timestamp, groundTruthPoints[0].timestamp);
+    EXPECT_GT(groundTruthPoints[2].timestamp, groundTruthPoints[1].timestamp);
+}
+
+TEST(GenerateCircularArcGroundTruthTest, CounterclockwiseSquare) {
+    // Generate points in a counterclockwise unit square starting pointing right.
+    const std::vector<GroundTruthPoint> groundTruthPoints = generateCircularArcGroundTruthPoints(
+            /*initialPosition=*/Eigen::Vector2f(10, 100),
+            /*initialAngle=*/M_PI_2,
+            /*velocity=*/1.0f,
+            /*turningAngle=*/M_PI_2,
+            /*numPoints=*/5);
+
+    ASSERT_EQ(5u, groundTruthPoints.size());
+    EXPECT_THAT(groundTruthPoints[0].position, Vector2fNear(Eigen::Vector2f(10, 100), 1e-6));
+    EXPECT_THAT(groundTruthPoints[1].position, Vector2fNear(Eigen::Vector2f(10, 101), 1e-6));
+    EXPECT_THAT(groundTruthPoints[2].position, Vector2fNear(Eigen::Vector2f(9, 101), 1e-6));
+    EXPECT_THAT(groundTruthPoints[3].position, Vector2fNear(Eigen::Vector2f(9, 100), 1e-6));
+    EXPECT_THAT(groundTruthPoints[4].position, Vector2fNear(Eigen::Vector2f(10, 100), 1e-6));
+}
+
+// --- Prediction-generation helper functions. ---
+
+// Creates a sequence of predictions with values equal to those of the given GroundTruthPoint.
+std::vector<PredictionPoint> generateConstantPredictions(const GroundTruthPoint& groundTruthPoint) {
+    std::vector<PredictionPoint> predictions;
+    nsecs_t predictionTimestamp = groundTruthPoint.timestamp + TEST_PREDICTION_INTERVAL_NANOS;
+    for (size_t j = 0; j < TEST_MAX_NUM_PREDICTIONS; ++j) {
+        predictions.push_back(PredictionPoint{{.position = groundTruthPoint.position,
+                                               .pressure = groundTruthPoint.pressure},
+                                              .originTimestamp = groundTruthPoint.timestamp,
+                                              .targetTimestamp = predictionTimestamp});
+        predictionTimestamp += TEST_PREDICTION_INTERVAL_NANOS;
+    }
+    return predictions;
+}
+
+// Generates TEST_MAX_NUM_PREDICTIONS predictions from the given most recent two ground truth points
+// by linear extrapolation of position and pressure. The interval between consecutive predictions'
+// timestamps is TEST_PREDICTION_INTERVAL_NANOS.
+std::vector<PredictionPoint> generatePredictionsByLinearExtrapolation(
+        const GroundTruthPoint& firstGroundTruth, const GroundTruthPoint& secondGroundTruth) {
+    // Precompute deltas.
+    const Eigen::Vector2f trajectory = secondGroundTruth.position - firstGroundTruth.position;
+    const float deltaPressure = secondGroundTruth.pressure - firstGroundTruth.pressure;
+    // Compute predictions.
+    std::vector<PredictionPoint> predictions;
+    Eigen::Vector2f predictionPosition = secondGroundTruth.position;
+    float predictionPressure = secondGroundTruth.pressure;
+    nsecs_t predictionTargetTimestamp = secondGroundTruth.timestamp;
+    for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS; ++i) {
+        predictionPosition += trajectory;
+        predictionPressure += deltaPressure;
+        predictionTargetTimestamp += TEST_PREDICTION_INTERVAL_NANOS;
+        predictions.push_back(
+                PredictionPoint{{.position = predictionPosition, .pressure = predictionPressure},
+                                .originTimestamp = secondGroundTruth.timestamp,
+                                .targetTimestamp = predictionTargetTimestamp});
+    }
+    return predictions;
+}
+
+TEST(GeneratePredictionsTest, GenerateConstantPredictions) {
+    const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10, 20), .pressure = 0.3f},
+                                            .timestamp = TEST_INITIAL_TIMESTAMP};
+    const std::vector<PredictionPoint> predictionPoints =
+            generateConstantPredictions(groundTruthPoint);
+
+    ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, predictionPoints.size());
+    for (size_t i = 0; i < predictionPoints.size(); ++i) {
+        SCOPED_TRACE(testing::Message() << "i = " << i);
+        EXPECT_THAT(predictionPoints[i].position, Vector2fNear(groundTruthPoint.position, 1e-6));
+        EXPECT_THAT(predictionPoints[i].pressure, FloatNear(groundTruthPoint.pressure, 1e-6));
+        EXPECT_EQ(predictionPoints[i].originTimestamp, groundTruthPoint.timestamp);
+        EXPECT_EQ(predictionPoints[i].targetTimestamp,
+                  groundTruthPoint.timestamp +
+                          static_cast<nsecs_t>(i + 1) * TEST_PREDICTION_INTERVAL_NANOS);
+    }
+}
+
+TEST(GeneratePredictionsTest, LinearExtrapolationFromTwoPoints) {
+    const nsecs_t initialTimestamp = TEST_INITIAL_TIMESTAMP;
+    const std::vector<PredictionPoint> predictionPoints = generatePredictionsByLinearExtrapolation(
+            GroundTruthPoint{{.position = Eigen::Vector2f(100, 200), .pressure = 0.9f},
+                             .timestamp = initialTimestamp},
+            GroundTruthPoint{{.position = Eigen::Vector2f(105, 190), .pressure = 0.8f},
+                             .timestamp = initialTimestamp + TEST_PREDICTION_INTERVAL_NANOS});
+
+    ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, predictionPoints.size());
+    const nsecs_t originTimestamp = initialTimestamp + TEST_PREDICTION_INTERVAL_NANOS;
+    EXPECT_THAT(predictionPoints[0],
+                PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(110, 180),
+                                                     .pressure = 0.7f},
+                                                    .originTimestamp = originTimestamp,
+                                                    .targetTimestamp = originTimestamp +
+                                                            TEST_PREDICTION_INTERVAL_NANOS},
+                                    0.001));
+    EXPECT_THAT(predictionPoints[1],
+                PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(115, 170),
+                                                     .pressure = 0.6f},
+                                                    .originTimestamp = originTimestamp,
+                                                    .targetTimestamp = originTimestamp +
+                                                            2 * TEST_PREDICTION_INTERVAL_NANOS},
+                                    0.001));
+    EXPECT_THAT(predictionPoints[2],
+                PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(120, 160),
+                                                     .pressure = 0.5f},
+                                                    .originTimestamp = originTimestamp,
+                                                    .targetTimestamp = originTimestamp +
+                                                            3 * TEST_PREDICTION_INTERVAL_NANOS},
+                                    0.001));
+    EXPECT_THAT(predictionPoints[3],
+                PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(125, 150),
+                                                     .pressure = 0.4f},
+                                                    .originTimestamp = originTimestamp,
+                                                    .targetTimestamp = originTimestamp +
+                                                            4 * TEST_PREDICTION_INTERVAL_NANOS},
+                                    0.001));
+    EXPECT_THAT(predictionPoints[4],
+                PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(130, 140),
+                                                     .pressure = 0.3f},
+                                                    .originTimestamp = originTimestamp,
+                                                    .targetTimestamp = originTimestamp +
+                                                            5 * TEST_PREDICTION_INTERVAL_NANOS},
+                                    0.001));
+}
+
+// Generates predictions by linear extrapolation for each consecutive pair of ground truth points
+// (see the comment for the above function for further explanation). Returns a vector of vectors of
+// prediction points, where the first index is the source ground truth index, and the second is the
+// prediction target index.
+//
+// The returned vector has size equal to the input vector, and the first element of the returned
+// vector is always empty.
+std::vector<std::vector<PredictionPoint>> generateAllPredictionsByLinearExtrapolation(
+        const std::vector<GroundTruthPoint>& groundTruthPoints) {
+    std::vector<std::vector<PredictionPoint>> allPredictions;
+    allPredictions.emplace_back();
+    for (size_t i = 1; i < groundTruthPoints.size(); ++i) {
+        allPredictions.push_back(generatePredictionsByLinearExtrapolation(groundTruthPoints[i - 1],
+                                                                          groundTruthPoints[i]));
+    }
+    return allPredictions;
+}
+
+TEST(GeneratePredictionsTest, GenerateAllPredictions) {
+    const nsecs_t initialTimestamp = TEST_INITIAL_TIMESTAMP;
+    std::vector<GroundTruthPoint>
+            groundTruthPoints{GroundTruthPoint{{.position = Eigen::Vector2f(0, 0),
+                                                .pressure = 0.5f},
+                                               .timestamp = initialTimestamp},
+                              GroundTruthPoint{{.position = Eigen::Vector2f(1, -1),
+                                                .pressure = 0.51f},
+                                               .timestamp = initialTimestamp +
+                                                       2 * TEST_PREDICTION_INTERVAL_NANOS},
+                              GroundTruthPoint{{.position = Eigen::Vector2f(2, -2),
+                                                .pressure = 0.52f},
+                                               .timestamp = initialTimestamp +
+                                                       3 * TEST_PREDICTION_INTERVAL_NANOS}};
+
+    const std::vector<std::vector<PredictionPoint>> allPredictions =
+            generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
+
+    // Check format of allPredictions data.
+    ASSERT_EQ(groundTruthPoints.size(), allPredictions.size());
+    EXPECT_TRUE(allPredictions[0].empty());
+    EXPECT_EQ(TEST_MAX_NUM_PREDICTIONS, allPredictions[1].size());
+    EXPECT_EQ(TEST_MAX_NUM_PREDICTIONS, allPredictions[2].size());
+
+    // Check positions of predictions generated from first pair of ground truth points.
+    EXPECT_THAT(allPredictions[1][0].position, Vector2fNear(Eigen::Vector2f(2, -2), 1e-9));
+    EXPECT_THAT(allPredictions[1][1].position, Vector2fNear(Eigen::Vector2f(3, -3), 1e-9));
+    EXPECT_THAT(allPredictions[1][2].position, Vector2fNear(Eigen::Vector2f(4, -4), 1e-9));
+    EXPECT_THAT(allPredictions[1][3].position, Vector2fNear(Eigen::Vector2f(5, -5), 1e-9));
+    EXPECT_THAT(allPredictions[1][4].position, Vector2fNear(Eigen::Vector2f(6, -6), 1e-9));
+
+    // Check pressures of predictions generated from first pair of ground truth points.
+    EXPECT_FLOAT_EQ(0.52f, allPredictions[1][0].pressure);
+    EXPECT_FLOAT_EQ(0.53f, allPredictions[1][1].pressure);
+    EXPECT_FLOAT_EQ(0.54f, allPredictions[1][2].pressure);
+    EXPECT_FLOAT_EQ(0.55f, allPredictions[1][3].pressure);
+    EXPECT_FLOAT_EQ(0.56f, allPredictions[1][4].pressure);
+}
+
+// --- Prediction error helper functions. ---
+
+struct GeneralPositionErrors {
+    float alongTrajectoryErrorMean;
+    float alongTrajectoryErrorStd;
+    float offTrajectoryRmse;
+};
+
+// Inputs:
+//  • Vector of ground truth points
+//  • Vector of vectors of prediction points, where the first index is the source ground truth
+//    index, and the second is the prediction target index.
+//
+// Returns a vector of GeneralPositionErrors, indexed by prediction time delta bucket.
+std::vector<GeneralPositionErrors> computeGeneralPositionErrors(
+        const std::vector<GroundTruthPoint>& groundTruthPoints,
+        const std::vector<std::vector<PredictionPoint>>& predictionPoints) {
+    // Aggregate errors by time bucket (prediction target index).
+    std::vector<GeneralPositionErrors> generalPostitionErrors;
+    for (size_t predictionTargetIndex = 0; predictionTargetIndex < TEST_MAX_NUM_PREDICTIONS;
+         ++predictionTargetIndex) {
+        std::vector<float> alongTrajectoryErrors;
+        std::vector<float> alongTrajectorySquaredErrors;
+        std::vector<float> offTrajectoryErrors;
+        for (size_t sourceGroundTruthIndex = 1; sourceGroundTruthIndex < groundTruthPoints.size();
+             ++sourceGroundTruthIndex) {
+            const size_t targetGroundTruthIndex =
+                    sourceGroundTruthIndex + predictionTargetIndex + 1;
+            // Only include errors for points with a ground truth value.
+            if (targetGroundTruthIndex < groundTruthPoints.size()) {
+                const Eigen::Vector2f trajectory =
+                        (groundTruthPoints[targetGroundTruthIndex].position -
+                         groundTruthPoints[targetGroundTruthIndex - 1].position)
+                                .normalized();
+                const Eigen::Vector2f orthogonalTrajectory =
+                        Eigen::Rotation2Df(M_PI_2) * trajectory;
+                const Eigen::Vector2f positionError =
+                        predictionPoints[sourceGroundTruthIndex][predictionTargetIndex].position -
+                        groundTruthPoints[targetGroundTruthIndex].position;
+                alongTrajectoryErrors.push_back(positionError.dot(trajectory));
+                alongTrajectorySquaredErrors.push_back(alongTrajectoryErrors.back() *
+                                                       alongTrajectoryErrors.back());
+                offTrajectoryErrors.push_back(positionError.dot(orthogonalTrajectory));
+            }
+        }
+        generalPostitionErrors.push_back(
+                {.alongTrajectoryErrorMean = average(alongTrajectoryErrors),
+                 .alongTrajectoryErrorStd = standardDeviation(alongTrajectoryErrors),
+                 .offTrajectoryRmse = rmse(offTrajectoryErrors)});
+    }
+    return generalPostitionErrors;
+}
+
+// Inputs:
+//  • Vector of ground truth points
+//  • Vector of vectors of prediction points, where the first index is the source ground truth
+//    index, and the second is the prediction target index.
+//
+// Returns a vector of pressure RMSEs, indexed by prediction time delta bucket.
+std::vector<float> computePressureRmses(
+        const std::vector<GroundTruthPoint>& groundTruthPoints,
+        const std::vector<std::vector<PredictionPoint>>& predictionPoints) {
+    // Aggregate errors by time bucket (prediction target index).
+    std::vector<float> pressureRmses;
+    for (size_t predictionTargetIndex = 0; predictionTargetIndex < TEST_MAX_NUM_PREDICTIONS;
+         ++predictionTargetIndex) {
+        std::vector<float> pressureErrors;
+        for (size_t sourceGroundTruthIndex = 1; sourceGroundTruthIndex < groundTruthPoints.size();
+             ++sourceGroundTruthIndex) {
+            const size_t targetGroundTruthIndex =
+                    sourceGroundTruthIndex + predictionTargetIndex + 1;
+            // Only include errors for points with a ground truth value.
+            if (targetGroundTruthIndex < groundTruthPoints.size()) {
+                pressureErrors.push_back(
+                        predictionPoints[sourceGroundTruthIndex][predictionTargetIndex].pressure -
+                        groundTruthPoints[targetGroundTruthIndex].pressure);
+            }
+        }
+        pressureRmses.push_back(rmse(pressureErrors));
+    }
+    return pressureRmses;
+}
+
+TEST(ErrorComputationHelperTest, ComputeGeneralPositionErrorsSimpleTest) {
+    std::vector<GroundTruthPoint> groundTruthPoints =
+            generateConstantGroundTruthPoints(GroundTruthPoint{{.position = Eigen::Vector2f(0, 0),
+                                                                .pressure = 0.0f},
+                                                               .timestamp = TEST_INITIAL_TIMESTAMP},
+                                              /*numPoints=*/TEST_MAX_NUM_PREDICTIONS + 2);
+    groundTruthPoints[3].position = Eigen::Vector2f(1, 0);
+    groundTruthPoints[4].position = Eigen::Vector2f(1, 1);
+    groundTruthPoints[5].position = Eigen::Vector2f(1, 3);
+    groundTruthPoints[6].position = Eigen::Vector2f(2, 3);
+
+    std::vector<std::vector<PredictionPoint>> predictionPoints =
+            generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
+
+    // The generated predictions look like:
+    //
+    // |    Source  |         Target Ground Truth Index          |
+    // |     Index  |   2    |   3    |   4    |   5    |   6    |
+    // |------------|--------|--------|--------|--------|--------|
+    // |          1 | (0, 0) | (0, 0) | (0, 0) | (0, 0) | (0, 0) |
+    // |          2 |        | (0, 0) | (0, 0) | (0, 0) | (0, 0) |
+    // |          3 |        |        | (2, 0) | (3, 0) | (4, 0) |
+    // |          4 |        |        |        | (1, 2) | (1, 3) |
+    // |          5 |        |        |        |        | (1, 5) |
+    // |---------------------------------------------------------|
+    // |               Actual Ground Truth Values                |
+    // |  Position  | (0, 0) | (1, 0) | (1, 1) | (1, 3) | (2, 3) |
+    // |  Previous  | (0, 0) | (0, 0) | (1, 0) | (1, 1) | (1, 3) |
+    //
+    // Note: this table organizes prediction targets by target ground truth index. Metrics are
+    // aggregated across points with the same prediction time bucket index, which is different.
+    // Each down-right diagonal from this table gives us points from a unique time bucket.
+
+    // Initialize expected prediction errors from the table above. The first time bucket corresponds
+    // to the long diagonal of the table, and subsequent time buckets step up-right from there.
+    const std::vector<std::vector<float>> expectedAlongTrajectoryErrors{{0, -1, -1, -1, -1},
+                                                                        {-1, -1, -3, -1},
+                                                                        {-1, -3, 2},
+                                                                        {-3, -2},
+                                                                        {-2}};
+    const std::vector<std::vector<float>> expectedOffTrajectoryErrors{{0, 0, 1, 0, 2},
+                                                                      {0, 1, 2, 0},
+                                                                      {1, 1, 3},
+                                                                      {1, 3},
+                                                                      {3}};
+
+    std::vector<GeneralPositionErrors> generalPositionErrors =
+            computeGeneralPositionErrors(groundTruthPoints, predictionPoints);
+
+    ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, generalPositionErrors.size());
+    for (size_t i = 0; i < generalPositionErrors.size(); ++i) {
+        SCOPED_TRACE(testing::Message() << "i = " << i);
+        EXPECT_FLOAT_EQ(average(expectedAlongTrajectoryErrors[i]),
+                        generalPositionErrors[i].alongTrajectoryErrorMean);
+        EXPECT_FLOAT_EQ(standardDeviation(expectedAlongTrajectoryErrors[i]),
+                        generalPositionErrors[i].alongTrajectoryErrorStd);
+        EXPECT_FLOAT_EQ(rmse(expectedOffTrajectoryErrors[i]),
+                        generalPositionErrors[i].offTrajectoryRmse);
+    }
+}
+
+TEST(ErrorComputationHelperTest, ComputePressureRmsesSimpleTest) {
+    // Generate ground truth points with pressures {0.0, 0.0, 0.0, 0.0, 0.5, 0.5, 0.5}.
+    // (We need TEST_MAX_NUM_PREDICTIONS + 2 to test all prediction time buckets.)
+    std::vector<GroundTruthPoint> groundTruthPoints =
+            generateConstantGroundTruthPoints(GroundTruthPoint{{.position = Eigen::Vector2f(0, 0),
+                                                                .pressure = 0.0f},
+                                                               .timestamp = TEST_INITIAL_TIMESTAMP},
+                                              /*numPoints=*/TEST_MAX_NUM_PREDICTIONS + 2);
+    for (size_t i = 4; i < groundTruthPoints.size(); ++i) {
+        groundTruthPoints[i].pressure = 0.5f;
+    }
+
+    std::vector<std::vector<PredictionPoint>> predictionPoints =
+            generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
+
+    std::vector<float> pressureRmses = computePressureRmses(groundTruthPoints, predictionPoints);
+
+    ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, pressureRmses.size());
+    EXPECT_FLOAT_EQ(rmse(std::vector<float>{0.0f, 0.0f, -0.5f, 0.5f, 0.0f}), pressureRmses[0]);
+    EXPECT_FLOAT_EQ(rmse(std::vector<float>{0.0f, -0.5f, -0.5f, 1.0f}), pressureRmses[1]);
+    EXPECT_FLOAT_EQ(rmse(std::vector<float>{-0.5f, -0.5f, -0.5f}), pressureRmses[2]);
+    EXPECT_FLOAT_EQ(rmse(std::vector<float>{-0.5f, -0.5f}), pressureRmses[3]);
+    EXPECT_FLOAT_EQ(rmse(std::vector<float>{-0.5f}), pressureRmses[4]);
+}
+
+// --- MotionPredictorMetricsManager tests. ---
+
+// Helper function that instantiates a MetricsManager with the given mock logged AtomFields. Takes
+// vectors of ground truth and prediction points of the same length, and passes these points to the
+// MetricsManager. The format of these vectors is expected to be:
+//  • groundTruthPoints: chronologically-ordered ground truth points, with at least 2 elements.
+//  • predictionPoints: the first index points to a vector of predictions corresponding to the
+//    source ground truth point with the same index.
+//     - The first element should be empty, because there are not expected to be predictions until
+//       we have received 2 ground truth points.
+//     - The last element may be empty, because there will be no future ground truth points to
+//       associate with those predictions (if not empty, it will be ignored).
+//     - To test all prediction buckets, there should be at least TEST_MAX_NUM_PREDICTIONS non-empty
+//       prediction sets (that is, excluding the first and last). Thus, groundTruthPoints and
+//       predictionPoints should have size at least TEST_MAX_NUM_PREDICTIONS + 2.
+//
+// The passed-in outAtomFields will contain the logged AtomFields when the function returns.
+//
+// This function returns void so that it can use test assertions.
+void runMetricsManager(const std::vector<GroundTruthPoint>& groundTruthPoints,
+                       const std::vector<std::vector<PredictionPoint>>& predictionPoints,
+                       std::vector<AtomFields>& outAtomFields) {
+    MotionPredictorMetricsManager metricsManager(TEST_PREDICTION_INTERVAL_NANOS,
+                                                 TEST_MAX_NUM_PREDICTIONS);
+    metricsManager.setMockLoggedAtomFields(&outAtomFields);
+
+    // Validate structure of groundTruthPoints and predictionPoints.
+    ASSERT_EQ(predictionPoints.size(), groundTruthPoints.size());
+    ASSERT_GE(groundTruthPoints.size(), 2u);
+    ASSERT_EQ(predictionPoints[0].size(), 0u);
+    for (size_t i = 1; i + 1 < predictionPoints.size(); ++i) {
+        SCOPED_TRACE(testing::Message() << "i = " << i);
+        ASSERT_EQ(predictionPoints[i].size(), TEST_MAX_NUM_PREDICTIONS);
+    }
+
+    // Pass ground truth points and predictions (for all except first and last ground truth).
+    for (size_t i = 0; i < groundTruthPoints.size(); ++i) {
+        metricsManager.onRecord(makeMotionEvent(groundTruthPoints[i]));
+        if ((i > 0) && (i + 1 < predictionPoints.size())) {
+            metricsManager.onPredict(makeMotionEvent(predictionPoints[i]));
+        }
+    }
+    // Send a stroke-end event to trigger the logging call.
+    metricsManager.onRecord(makeLiftMotionEvent());
+}
+
+// Vacuous test:
+//  • Input: no prediction data.
+//  • Expectation: no metrics should be logged.
+TEST(MotionPredictorMetricsManagerTest, NoPredictions) {
+    std::vector<AtomFields> mockLoggedAtomFields;
+    MotionPredictorMetricsManager metricsManager(TEST_PREDICTION_INTERVAL_NANOS,
+                                                 TEST_MAX_NUM_PREDICTIONS);
+    metricsManager.setMockLoggedAtomFields(&mockLoggedAtomFields);
+
+    metricsManager.onRecord(makeMotionEvent(
+            GroundTruthPoint{{.position = Eigen::Vector2f(0, 0), .pressure = 0}, .timestamp = 0}));
+    metricsManager.onRecord(makeLiftMotionEvent());
+
+    // Check that mockLoggedAtomFields is still empty (as it was initialized empty), ensuring that
+    // no metrics were logged.
+    EXPECT_EQ(0u, mockLoggedAtomFields.size());
+}
+
+// Perfect predictions test:
+//  • Input: constant input events, perfect predictions matching the input events.
+//  • Expectation: all error metrics should be zero, or NO_DATA_SENTINEL for "unreported" metrics.
+//    (For example, scale-invariant errors are only reported for the final time bucket.)
+TEST(MotionPredictorMetricsManagerTest, ConstantGroundTruthPerfectPredictions) {
+    GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10.0f, 20.0f), .pressure = 0.6f},
+                                      .timestamp = TEST_INITIAL_TIMESTAMP};
+
+    // Generate ground truth and prediction points as described by the runMetricsManager comment.
+    std::vector<GroundTruthPoint> groundTruthPoints;
+    std::vector<std::vector<PredictionPoint>> predictionPoints;
+    for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS + 2; ++i) {
+        groundTruthPoints.push_back(groundTruthPoint);
+        predictionPoints.push_back(i > 0 ? generateConstantPredictions(groundTruthPoint)
+                                         : std::vector<PredictionPoint>{});
+        groundTruthPoint.timestamp += TEST_PREDICTION_INTERVAL_NANOS;
+    }
+
+    std::vector<AtomFields> atomFields;
+    runMetricsManager(groundTruthPoints, predictionPoints, atomFields);
+
+    ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, atomFields.size());
+    // Check that errors are all zero, or NO_DATA_SENTINEL for unreported metrics.
+    for (size_t i = 0; i < atomFields.size(); ++i) {
+        SCOPED_TRACE(testing::Message() << "i = " << i);
+        const AtomFields& atom = atomFields[i];
+        const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
+        EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
+        // General errors: reported for every time bucket.
+        EXPECT_EQ(0, atom.alongTrajectoryErrorMeanMillipixels);
+        EXPECT_EQ(0, atom.alongTrajectoryErrorStdMillipixels);
+        EXPECT_EQ(0, atom.offTrajectoryRmseMillipixels);
+        EXPECT_EQ(0, atom.pressureRmseMilliunits);
+        // High-velocity errors: reported only for the last two time buckets.
+        // However, this data has zero velocity, so these metrics should all be NO_DATA_SENTINEL.
+        EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityAlongTrajectoryRmse);
+        EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityOffTrajectoryRmse);
+        // Scale-invariant errors: reported only for the last time bucket.
+        if (i + 1 == atomFields.size()) {
+            EXPECT_EQ(0, atom.scaleInvariantAlongTrajectoryRmse);
+            EXPECT_EQ(0, atom.scaleInvariantOffTrajectoryRmse);
+        } else {
+            EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantAlongTrajectoryRmse);
+            EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantOffTrajectoryRmse);
+        }
+    }
+}
+
+TEST(MotionPredictorMetricsManagerTest, QuadraticPressureLinearPredictions) {
+    // Generate ground truth points.
+    //
+    // Ground truth pressures are a quadratically increasing function from some initial value.
+    const float initialPressure = 0.5f;
+    const float quadraticCoefficient = 0.01f;
+    std::vector<GroundTruthPoint> groundTruthPoints;
+    nsecs_t timestamp = TEST_INITIAL_TIMESTAMP;
+    // As described in the runMetricsManager comment, we should have TEST_MAX_NUM_PREDICTIONS + 2
+    // ground truth points.
+    for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS + 2; ++i) {
+        const float pressure = initialPressure + quadraticCoefficient * static_cast<float>(i * i);
+        groundTruthPoints.push_back(
+                GroundTruthPoint{{.position = Eigen::Vector2f(0, 0), .pressure = pressure},
+                                 .timestamp = timestamp});
+        timestamp += TEST_PREDICTION_INTERVAL_NANOS;
+    }
+
+    // Note: the first index is the source ground truth index, and the second is the prediction
+    // target index.
+    std::vector<std::vector<PredictionPoint>> predictionPoints =
+            generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
+
+    const std::vector<float> pressureErrors =
+            computePressureRmses(groundTruthPoints, predictionPoints);
+
+    // Run test.
+    std::vector<AtomFields> atomFields;
+    runMetricsManager(groundTruthPoints, predictionPoints, atomFields);
+
+    // Check logged metrics match expectations.
+    ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, atomFields.size());
+    for (size_t i = 0; i < atomFields.size(); ++i) {
+        SCOPED_TRACE(testing::Message() << "i = " << i);
+        const AtomFields& atom = atomFields[i];
+        // Check time bucket delta matches expectation based on index and prediction interval.
+        const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
+        EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
+        // Check pressure error matches expectation.
+        EXPECT_NEAR(static_cast<int>(1000 * pressureErrors[i]), atom.pressureRmseMilliunits, 1);
+    }
+}
+
+TEST(MotionPredictorMetricsManagerTest, QuadraticPositionLinearPredictionsGeneralErrors) {
+    // Generate ground truth points.
+    //
+    // Each component of the ground truth positions are an independent quadratically increasing
+    // function from some initial value.
+    const Eigen::Vector2f initialPosition(200, 300);
+    const Eigen::Vector2f quadraticCoefficients(-2, 3);
+    std::vector<GroundTruthPoint> groundTruthPoints;
+    nsecs_t timestamp = TEST_INITIAL_TIMESTAMP;
+    // As described in the runMetricsManager comment, we should have TEST_MAX_NUM_PREDICTIONS + 2
+    // ground truth points.
+    for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS + 2; ++i) {
+        const Eigen::Vector2f position =
+                initialPosition + quadraticCoefficients * static_cast<float>(i * i);
+        groundTruthPoints.push_back(
+                GroundTruthPoint{{.position = position, .pressure = 0.5}, .timestamp = timestamp});
+        timestamp += TEST_PREDICTION_INTERVAL_NANOS;
+    }
+
+    // Note: the first index is the source ground truth index, and the second is the prediction
+    // target index.
+    std::vector<std::vector<PredictionPoint>> predictionPoints =
+            generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
+
+    std::vector<GeneralPositionErrors> generalPositionErrors =
+            computeGeneralPositionErrors(groundTruthPoints, predictionPoints);
+
+    // Run test.
+    std::vector<AtomFields> atomFields;
+    runMetricsManager(groundTruthPoints, predictionPoints, atomFields);
+
+    // Check logged metrics match expectations.
+    ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, atomFields.size());
+    for (size_t i = 0; i < atomFields.size(); ++i) {
+        SCOPED_TRACE(testing::Message() << "i = " << i);
+        const AtomFields& atom = atomFields[i];
+        // Check time bucket delta matches expectation based on index and prediction interval.
+        const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
+        EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
+        // Check general position errors match expectation.
+        EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].alongTrajectoryErrorMean),
+                    atom.alongTrajectoryErrorMeanMillipixels, 1);
+        EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].alongTrajectoryErrorStd),
+                    atom.alongTrajectoryErrorStdMillipixels, 1);
+        EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].offTrajectoryRmse),
+                    atom.offTrajectoryRmseMillipixels, 1);
+    }
+}
+
+// Counterclockwise regular octagonal section test:
+//  • Input – ground truth: constantly-spaced input events starting at a trajectory pointing exactly
+//    rightwards, and rotating by 45° counterclockwise after each input.
+//  • Input – predictions: simple linear extrapolations of previous two ground truth points.
+//
+// The code below uses the following terminology to distinguish references to ground truth events:
+//  • Source ground truth: the most recent ground truth point received at the time the prediction
+//    was made.
+//  • Target ground truth: the ground truth event that the prediction was attempting to match.
+TEST(MotionPredictorMetricsManagerTest, CounterclockwiseOctagonGroundTruthLinearPredictions) {
+    // Select a stroke velocity that exceeds the high-velocity threshold of 1100 px/sec.
+    // For an input rate of 240 hz, 1100 px/sec * (1/240) sec/input ≈ 4.58 pixels per input.
+    const float strokeVelocity = 10; // pixels per input
+
+    // As described in the runMetricsManager comment, we should have TEST_MAX_NUM_PREDICTIONS + 2
+    // ground truth points.
+    std::vector<GroundTruthPoint> groundTruthPoints = generateCircularArcGroundTruthPoints(
+            /*initialPosition=*/Eigen::Vector2f(100, 100),
+            /*initialAngle=*/M_PI_2,
+            /*velocity=*/strokeVelocity,
+            /*turningAngle=*/-M_PI_4,
+            /*numPoints=*/TEST_MAX_NUM_PREDICTIONS + 2);
+
+    std::vector<std::vector<PredictionPoint>> predictionPoints =
+            generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
+
+    std::vector<GeneralPositionErrors> generalPositionErrors =
+            computeGeneralPositionErrors(groundTruthPoints, predictionPoints);
+
+    // Run test.
+    std::vector<AtomFields> atomFields;
+    runMetricsManager(groundTruthPoints, predictionPoints, atomFields);
+
+    // Check logged metrics match expectations.
+    ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, atomFields.size());
+    for (size_t i = 0; i < atomFields.size(); ++i) {
+        SCOPED_TRACE(testing::Message() << "i = " << i);
+        const AtomFields& atom = atomFields[i];
+        const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
+        EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
+
+        // General errors: reported for every time bucket.
+        EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].alongTrajectoryErrorMean),
+                    atom.alongTrajectoryErrorMeanMillipixels, 1);
+        // We allow for some floating point error in standard deviation (0.02 pixels).
+        EXPECT_NEAR(1000 * generalPositionErrors[i].alongTrajectoryErrorStd,
+                    atom.alongTrajectoryErrorStdMillipixels, 20);
+        // All position errors are equal, so the standard deviation should be approximately zero.
+        EXPECT_NEAR(0, atom.alongTrajectoryErrorStdMillipixels, 20);
+        // Absolute value for RMSE, since it must be non-negative.
+        EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].offTrajectoryRmse),
+                    atom.offTrajectoryRmseMillipixels, 1);
+
+        // High-velocity errors: reported only for the last two time buckets.
+        //
+        // Since our input stroke velocity is chosen to be above the high-velocity threshold, all
+        // data contributes to high-velocity errors, and thus high-velocity errors should be equal
+        // to general errors (where reported).
+        //
+        // As above, use absolute value for RMSE, since it must be non-negative.
+        if (i + 2 >= atomFields.size()) {
+            EXPECT_NEAR(static_cast<int>(
+                                1000 * std::abs(generalPositionErrors[i].alongTrajectoryErrorMean)),
+                        atom.highVelocityAlongTrajectoryRmse, 1);
+            EXPECT_NEAR(static_cast<int>(1000 *
+                                         std::abs(generalPositionErrors[i].offTrajectoryRmse)),
+                        atom.highVelocityOffTrajectoryRmse, 1);
+        } else {
+            EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityAlongTrajectoryRmse);
+            EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityOffTrajectoryRmse);
+        }
+
+        // Scale-invariant errors: reported only for the last time bucket, where the reported value
+        // is the aggregation across all time buckets.
+        //
+        // The MetricsManager stores mMaxNumPredictions recent ground truth segments. Our ground
+        // truth segments here all have a length of strokeVelocity, so we can convert general errors
+        // to scale-invariant errors by dividing by `strokeVelocty * TEST_MAX_NUM_PREDICTIONS`.
+        //
+        // As above, use absolute value for RMSE, since it must be non-negative.
+        if (i + 1 == atomFields.size()) {
+            const float pathLength = strokeVelocity * TEST_MAX_NUM_PREDICTIONS;
+            std::vector<float> alongTrajectoryAbsoluteErrors;
+            std::vector<float> offTrajectoryAbsoluteErrors;
+            for (size_t j = 0; j < TEST_MAX_NUM_PREDICTIONS; ++j) {
+                alongTrajectoryAbsoluteErrors.push_back(
+                        std::abs(generalPositionErrors[j].alongTrajectoryErrorMean));
+                offTrajectoryAbsoluteErrors.push_back(
+                        std::abs(generalPositionErrors[j].offTrajectoryRmse));
+            }
+            EXPECT_NEAR(static_cast<int>(1000 * average(alongTrajectoryAbsoluteErrors) /
+                                         pathLength),
+                        atom.scaleInvariantAlongTrajectoryRmse, 1);
+            EXPECT_NEAR(static_cast<int>(1000 * average(offTrajectoryAbsoluteErrors) / pathLength),
+                        atom.scaleInvariantOffTrajectoryRmse, 1);
+        } else {
+            EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantAlongTrajectoryRmse);
+            EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantOffTrajectoryRmse);
+        }
+    }
+}
+
+} // namespace
+} // namespace android
diff --git a/libs/nativedisplay/surfacetexture/EGLConsumer.cpp b/libs/nativedisplay/surfacetexture/EGLConsumer.cpp
index 0128859..275b7a4 100644
--- a/libs/nativedisplay/surfacetexture/EGLConsumer.cpp
+++ b/libs/nativedisplay/surfacetexture/EGLConsumer.cpp
@@ -38,10 +38,10 @@
 namespace android {
 
 // Macros for including the SurfaceTexture name in log messages
-#define EGC_LOGV(x, ...) ALOGV("[%s] " x, st.mName.string(), ##__VA_ARGS__)
-#define EGC_LOGD(x, ...) ALOGD("[%s] " x, st.mName.string(), ##__VA_ARGS__)
-#define EGC_LOGW(x, ...) ALOGW("[%s] " x, st.mName.string(), ##__VA_ARGS__)
-#define EGC_LOGE(x, ...) ALOGE("[%s] " x, st.mName.string(), ##__VA_ARGS__)
+#define EGC_LOGV(x, ...) ALOGV("[%s] " x, st.mName.c_str(), ##__VA_ARGS__)
+#define EGC_LOGD(x, ...) ALOGD("[%s] " x, st.mName.c_str(), ##__VA_ARGS__)
+#define EGC_LOGW(x, ...) ALOGW("[%s] " x, st.mName.c_str(), ##__VA_ARGS__)
+#define EGC_LOGE(x, ...) ALOGE("[%s] " x, st.mName.c_str(), ##__VA_ARGS__)
 
 static const struct {
     uint32_t width, height;
diff --git a/libs/nativedisplay/surfacetexture/ImageConsumer.cpp b/libs/nativedisplay/surfacetexture/ImageConsumer.cpp
index cf16739..32b229d 100644
--- a/libs/nativedisplay/surfacetexture/ImageConsumer.cpp
+++ b/libs/nativedisplay/surfacetexture/ImageConsumer.cpp
@@ -19,7 +19,7 @@
 #include <surfacetexture/SurfaceTexture.h>
 
 // Macro for including the SurfaceTexture name in log messages
-#define IMG_LOGE(x, ...) ALOGE("[%s] " x, st.mName.string(), ##__VA_ARGS__)
+#define IMG_LOGE(x, ...) ALOGE("[%s] " x, st.mName.c_str(), ##__VA_ARGS__)
 
 namespace android {
 
diff --git a/libs/nativedisplay/surfacetexture/SurfaceTexture.cpp b/libs/nativedisplay/surfacetexture/SurfaceTexture.cpp
index d3d4cba..9f610e1 100644
--- a/libs/nativedisplay/surfacetexture/SurfaceTexture.cpp
+++ b/libs/nativedisplay/surfacetexture/SurfaceTexture.cpp
@@ -26,10 +26,10 @@
 namespace android {
 
 // Macros for including the SurfaceTexture name in log messages
-#define SFT_LOGV(x, ...) ALOGV("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define SFT_LOGD(x, ...) ALOGD("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define SFT_LOGW(x, ...) ALOGW("[%s] " x, mName.string(), ##__VA_ARGS__)
-#define SFT_LOGE(x, ...) ALOGE("[%s] " x, mName.string(), ##__VA_ARGS__)
+#define SFT_LOGV(x, ...) ALOGV("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define SFT_LOGD(x, ...) ALOGD("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define SFT_LOGW(x, ...) ALOGW("[%s] " x, mName.c_str(), ##__VA_ARGS__)
+#define SFT_LOGE(x, ...) ALOGE("[%s] " x, mName.c_str(), ##__VA_ARGS__)
 
 static const mat4 mtxIdentity;
 
diff --git a/libs/nativewindow/include/system/window.h b/libs/nativewindow/include/system/window.h
index edaa422..e158f01 100644
--- a/libs/nativewindow/include/system/window.h
+++ b/libs/nativewindow/include/system/window.h
@@ -1060,6 +1060,42 @@
     ANATIVEWINDOW_FRAME_RATE_MIN
 };
 
+/*
+ * Frame rate category values that can be used in Transaction::setFrameRateCategory.
+ */
+enum {
+    /**
+     * Default value. This value can also be set to return to default behavior, such as layers
+     * without animations.
+     */
+    ANATIVEWINDOW_FRAME_RATE_CATEGORY_DEFAULT = 0,
+
+    /**
+     * The layer will explicitly not influence the frame rate.
+     * This may indicate a frame rate suitable for no animation updates (such as a cursor blinking
+     * or a sporadic update).
+     */
+    ANATIVEWINDOW_FRAME_RATE_CATEGORY_NO_PREFERENCE = 1,
+
+    /**
+     * Indicates a frame rate suitable for animations that looks fine even if played at a low frame
+     * rate.
+     */
+    ANATIVEWINDOW_FRAME_RATE_CATEGORY_LOW = 2,
+
+    /**
+     * Indicates a middle frame rate suitable for animations that do not require higher frame
+     * rates, or do not benefit from high smoothness. This is normally 60 Hz or close to it.
+     */
+    ANATIVEWINDOW_FRAME_RATE_CATEGORY_NORMAL = 3,
+
+    /**
+     * Indicates a frame rate suitable for animations that require a high frame rate, which may
+     * increase smoothness but may also increase power usage.
+     */
+    ANATIVEWINDOW_FRAME_RATE_CATEGORY_HIGH = 4
+};
+
 static inline int native_window_set_frame_rate(struct ANativeWindow* window, float frameRate,
                                         int8_t compatibility, int8_t changeFrameRateStrategy) {
     return window->perform(window, NATIVE_WINDOW_SET_FRAME_RATE, (double)frameRate,
diff --git a/libs/renderengine/skia/AutoBackendTexture.cpp b/libs/renderengine/skia/AutoBackendTexture.cpp
index 90dcae4..02d1e1e 100644
--- a/libs/renderengine/skia/AutoBackendTexture.cpp
+++ b/libs/renderengine/skia/AutoBackendTexture.cpp
@@ -24,6 +24,7 @@
 #include <include/gpu/ganesh/SkImageGanesh.h>
 #include <include/gpu/ganesh/SkSurfaceGanesh.h>
 #include <include/gpu/ganesh/gl/GrGLBackendSurface.h>
+#include <include/gpu/vk/GrVkTypes.h>
 #include <android/hardware_buffer.h>
 #include "ColorSpaces.h"
 #include "log/log_main.h"
diff --git a/libs/renderengine/skia/Cache.cpp b/libs/renderengine/skia/Cache.cpp
index e1887a8..9d61d62 100644
--- a/libs/renderengine/skia/Cache.cpp
+++ b/libs/renderengine/skia/Cache.cpp
@@ -63,9 +63,12 @@
             .geometry =
                     Geometry{
                             .boundaries = rect,
-                            .roundedCornersCrop = rect,
                             .roundedCornersRadius = {50.f, 50.f},
+                            .roundedCornersCrop = rect,
                     },
+            .alpha = 1,
+            // setting this is mandatory for shadows and blurs
+            .skipContentDraw = true,
             // drawShadow ignores alpha
             .shadow =
                     ShadowSettings{
@@ -76,16 +79,13 @@
                             .lightRadius = 2500.0f,
                             .length = 15.f,
                     },
-            // setting this is mandatory for shadows and blurs
-            .skipContentDraw = true,
-            .alpha = 1,
     };
     LayerSettings caster{
             .geometry =
                     Geometry{
                             .boundaries = smallerRect,
-                            .roundedCornersCrop = rect,
                             .roundedCornersRadius = {50.f, 50.f},
+                            .roundedCornersCrop = rect,
                     },
             .source =
                     PixelSource{
@@ -126,10 +126,10 @@
     LayerSettings layer{
             .geometry =
                     Geometry{
+                            .boundaries = rect,
                             // The position transform doesn't matter when the reduced shader mode
                             // in in effect. A matrix transform stage is always included.
                             .positionTransform = mat4(),
-                            .boundaries = rect,
                             .roundedCornersCrop = rect,
                     },
             .source = PixelSource{.buffer =
@@ -263,11 +263,11 @@
     LayerSettings layer{
             .geometry =
                     Geometry{
+                            .boundaries = rect,
                             // Note that this flip matrix only makes a difference when clipping,
                             // which happens in this layer because the roundrect crop is just a bit
                             // larger than the layer bounds.
                             .positionTransform = kFlip,
-                            .boundaries = rect,
                             .roundedCornersRadius = {94.2551f, 94.2551f},
                             .roundedCornersCrop = FloatRect(-93.75, 0, displayRect.width() + 93.75,
                                                             displayRect.height()),
@@ -275,12 +275,12 @@
             .source = PixelSource{.buffer =
                                           Buffer{
                                                   .buffer = srcTexture,
-                                                  .maxLuminanceNits = 1000.f,
-                                                  .isOpaque = 0,
                                                   .usePremultipliedAlpha = 1,
+                                                  .isOpaque = 0,
+                                                  .maxLuminanceNits = 1000.f,
                                           }},
-            .sourceDataspace = kOtherDataSpace,
             .alpha = 1,
+            .sourceDataspace = kOtherDataSpace,
 
     };
 
@@ -296,10 +296,10 @@
     LayerSettings layer{
             .geometry =
                     Geometry{
-                            .positionTransform = kScaleAndTranslate,
                             // the boundaries have to be smaller than the rounded crop so that
                             // clipRRect is used instead of drawRRect
                             .boundaries = small,
+                            .positionTransform = kScaleAndTranslate,
                             .roundedCornersRadius = {50.f, 50.f},
                             .roundedCornersCrop = rect,
                     },
@@ -307,8 +307,8 @@
                     PixelSource{
                             .solidColor = half3(0.f, 0.f, 0.f),
                     },
-            .sourceDataspace = kDestDataSpace,
             .alpha = 0,
+            .sourceDataspace = kDestDataSpace,
             .disableBlending = true,
 
     };
diff --git a/libs/renderengine/skia/GLExtensions.cpp b/libs/renderengine/skia/GLExtensions.cpp
index 32da303..1d4d35f 100644
--- a/libs/renderengine/skia/GLExtensions.cpp
+++ b/libs/renderengine/skia/GLExtensions.cpp
@@ -68,19 +68,19 @@
 }
 
 char const* GLExtensions::getVendor() const {
-    return mVendor.string();
+    return mVendor.c_str();
 }
 
 char const* GLExtensions::getRenderer() const {
-    return mRenderer.string();
+    return mRenderer.c_str();
 }
 
 char const* GLExtensions::getVersion() const {
-    return mVersion.string();
+    return mVersion.c_str();
 }
 
 char const* GLExtensions::getExtensions() const {
-    return mExtensions.string();
+    return mExtensions.c_str();
 }
 
 void GLExtensions::initWithEGLStrings(char const* eglVersion, char const* eglExtensions) {
@@ -127,11 +127,11 @@
 }
 
 char const* GLExtensions::getEGLVersion() const {
-    return mEGLVersion.string();
+    return mEGLVersion.c_str();
 }
 
 char const* GLExtensions::getEGLExtensions() const {
-    return mEGLExtensions.string();
+    return mEGLExtensions.c_str();
 }
 
 } // namespace skia
diff --git a/libs/renderengine/skia/SkiaRenderEngine.cpp b/libs/renderengine/skia/SkiaRenderEngine.cpp
index ababa82..709de0d 100644
--- a/libs/renderengine/skia/SkiaRenderEngine.cpp
+++ b/libs/renderengine/skia/SkiaRenderEngine.cpp
@@ -20,7 +20,6 @@
 
 #include "SkiaRenderEngine.h"
 
-#include <include/gpu/ganesh/SkSurfaceGanesh.h>
 #include <GrBackendSemaphore.h>
 #include <GrContextOptions.h>
 #include <SkBlendMode.h>
@@ -55,13 +54,14 @@
 #include <android-base/stringprintf.h>
 #include <gui/FenceMonitor.h>
 #include <gui/TraceUtils.h>
+#include <include/gpu/ganesh/SkSurfaceGanesh.h>
 #include <pthread.h>
 #include <src/core/SkTraceEventCommon.h>
 #include <sync/sync.h>
 #include <ui/BlurRegion.h>
-#include <ui/DataspaceUtils.h>
 #include <ui/DebugUtils.h>
 #include <ui/GraphicBuffer.h>
+#include <ui/HdrRenderTypeUtils.h>
 #include <utils/Trace.h>
 
 #include <cmath>
@@ -396,12 +396,10 @@
     }
     // We don't attempt to map a buffer if the buffer contains protected content. In GL this is
     // important because GPU resources for protected buffers are much more limited. (In Vk we
-    // simply match the existing behavior for protected buffers.)  In Vk, we never cache any
-    // buffers while in a protected context, since Vk cannot share across contexts, and protected
-    // is less common.
+    // simply match the existing behavior for protected buffers.)  We also never cache any
+    // buffers while in a protected context.
     const bool isProtectedBuffer = buffer->getUsage() & GRALLOC_USAGE_PROTECTED;
-    if (isProtectedBuffer ||
-        (mRenderEngineType == RenderEngineType::SKIA_VK_THREADED && isProtected())) {
+    if (isProtectedBuffer || isProtected()) {
         return;
     }
     ATRACE_CALL();
@@ -466,9 +464,8 @@
 
 std::shared_ptr<AutoBackendTexture::LocalRef> SkiaRenderEngine::getOrCreateBackendTexture(
         const sp<GraphicBuffer>& buffer, bool isOutputBuffer) {
-    // Do not lookup the buffer in the cache for protected contexts with the SkiaVk back-end
-    if (mRenderEngineType == RenderEngineType::SKIA_GL_THREADED ||
-        (mRenderEngineType == RenderEngineType::SKIA_VK_THREADED && !isProtected())) {
+    // Do not lookup the buffer in the cache for protected contexts
+    if (!isProtected()) {
         if (const auto& it = mTextureCache.find(buffer->getId()); it != mTextureCache.end()) {
             return it->second;
         }
@@ -1024,7 +1021,10 @@
             // Most HDR standards require at least 10-bits of color depth for source content, so we
             // can just extract the transfer function rather than dig into precise gralloc layout.
             // Furthermore, we can assume that the only 8-bit target we support is RGBA8888.
-            const bool requiresDownsample = isHdrDataspace(layer.sourceDataspace) &&
+            const bool requiresDownsample =
+                    getHdrRenderType(layer.sourceDataspace,
+                                     std::optional<ui::PixelFormat>(static_cast<ui::PixelFormat>(
+                                             buffer->getPixelFormat()))) != HdrRenderType::SDR &&
                     buffer->getPixelFormat() == PIXEL_FORMAT_RGBA_8888;
             if (layerDimmingRatio <= kDimmingThreshold || requiresDownsample) {
                 paint.setDither(true);
diff --git a/libs/renderengine/skia/debug/record.sh b/libs/renderengine/skia/debug/record.sh
index e99b7ae..c818c40 100755
--- a/libs/renderengine/skia/debug/record.sh
+++ b/libs/renderengine/skia/debug/record.sh
@@ -16,7 +16,6 @@
   # first time use requires these changes
   adb root
   adb shell setenforce 0
-  adb shell setprop debug.renderengine.backend "skiaglthreaded"
   adb shell stop
   adb shell start
   exit 1;
diff --git a/libs/renderengine/skia/filters/KawaseBlurFilter.cpp b/libs/renderengine/skia/filters/KawaseBlurFilter.cpp
index 7bf2b0c..5c9820c 100644
--- a/libs/renderengine/skia/filters/KawaseBlurFilter.cpp
+++ b/libs/renderengine/skia/filters/KawaseBlurFilter.cpp
@@ -111,9 +111,9 @@
     constexpr int kSampleCount = 1;
     constexpr bool kMipmapped = false;
     constexpr SkSurfaceProps* kProps = nullptr;
-    sk_sp<SkSurface> surface =
-            SkSurfaces::RenderTarget(context, skgpu::Budgeted::kYes, scaledInfo, kSampleCount,
-                                     kTopLeft_GrSurfaceOrigin, kProps, kMipmapped);
+    sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(context, skgpu::Budgeted::kYes, scaledInfo,
+                                                        kSampleCount, kTopLeft_GrSurfaceOrigin,
+                                                        kProps, kMipmapped, input->isProtected());
     LOG_ALWAYS_FATAL_IF(!surface, "%s: Failed to create surface for blurring!", __func__);
     sk_sp<SkImage> tmpBlur = makeImage(surface.get(), &blurBuilder);
 
diff --git a/libs/sensor/Sensor.cpp b/libs/sensor/Sensor.cpp
index b6ea77d..a1549ea 100644
--- a/libs/sensor/Sensor.cpp
+++ b/libs/sensor/Sensor.cpp
@@ -74,7 +74,7 @@
         if (hwSensor.maxDelay > INT_MAX) {
             // Max delay is declared as a 64 bit integer for 64 bit architectures. But it should
             // always fit in a 32 bit integer, log error and cap it to INT_MAX.
-            ALOGE("Sensor maxDelay overflow error %s %" PRId64, mName.string(),
+            ALOGE("Sensor maxDelay overflow error %s %" PRId64, mName.c_str(),
                   static_cast<int64_t>(hwSensor.maxDelay));
             mMaxDelay = INT_MAX;
         } else {
@@ -335,7 +335,7 @@
         if (actualReportingMode != expectedReportingMode) {
             ALOGE("Reporting Mode incorrect: sensor %s handle=%#010" PRIx32 " type=%" PRId32 " "
                    "actual=%d expected=%d",
-                   mName.string(), mHandle, mType, actualReportingMode, expectedReportingMode);
+                   mName.c_str(), mHandle, mType, actualReportingMode, expectedReportingMode);
         }
     }
 
@@ -613,7 +613,7 @@
         const String8& string8) {
     uint32_t len = static_cast<uint32_t>(string8.length());
     FlattenableUtils::write(buffer, size, len);
-    memcpy(static_cast<char*>(buffer), string8.string(), len);
+    memcpy(static_cast<char*>(buffer), string8.c_str(), len);
     FlattenableUtils::advance(buffer, size, len);
     size -= FlattenableUtils::align<4>(buffer);
 }
@@ -627,7 +627,7 @@
     if (size < len) {
         return false;
     }
-    outputString8.setTo(static_cast<char const*>(buffer), len);
+    outputString8 = String8(static_cast<char const*>(buffer), len);
 
     if (size < FlattenableUtils::align<4>(len)) {
         ALOGE("Malformed Sensor String8 field. Should be in a 4-byte aligned buffer but is not.");
diff --git a/libs/shaders/tests/Android.bp b/libs/shaders/tests/Android.bp
index 1e4f45a..5639d74 100644
--- a/libs/shaders/tests/Android.bp
+++ b/libs/shaders/tests/Android.bp
@@ -37,6 +37,7 @@
     shared_libs: [
         "android.hardware.graphics.common@1.2",
         "libnativewindow",
+        "libbase",
     ],
     static_libs: [
         "libarect",
diff --git a/libs/tonemap/tests/Android.bp b/libs/tonemap/tests/Android.bp
index 2abf515..5c5fc6c 100644
--- a/libs/tonemap/tests/Android.bp
+++ b/libs/tonemap/tests/Android.bp
@@ -36,6 +36,7 @@
     ],
     shared_libs: [
         "libnativewindow",
+        "libbase",
     ],
     static_libs: [
         "libmath",
diff --git a/libs/ui/Fence.cpp b/libs/ui/Fence.cpp
index cc96f83..4be0a3a 100644
--- a/libs/ui/Fence.cpp
+++ b/libs/ui/Fence.cpp
@@ -115,7 +115,7 @@
 
 sp<Fence> Fence::merge(const String8& name, const sp<Fence>& f1,
         const sp<Fence>& f2) {
-    return merge(name.string(), f1, f2);
+    return merge(name.c_str(), f1, f2);
 }
 
 int Fence::dup() const {
diff --git a/libs/ui/include_types/ui/HdrRenderTypeUtils.h b/libs/ui/include_types/ui/HdrRenderTypeUtils.h
new file mode 100644
index 0000000..b0af878
--- /dev/null
+++ b/libs/ui/include_types/ui/HdrRenderTypeUtils.h
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2021 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 <ui/GraphicTypes.h>
+
+namespace android {
+
+enum class HdrRenderType {
+    SDR,         // just render to SDR
+    DISPLAY_HDR, // HDR by extended brightness
+    GENERIC_HDR  // tonemapped HDR
+};
+
+/***
+ * A helper function to classify how we treat the result based on params.
+ *
+ * @param dataspace the dataspace
+ * @param pixelFormat optional, in case there is no source buffer.
+ * @param hdrSdrRatio default is 1.f, render engine side doesn't take care of it.
+ * @return HdrRenderType
+ */
+inline HdrRenderType getHdrRenderType(ui::Dataspace dataspace,
+                                      std::optional<ui::PixelFormat> pixelFormat,
+                                      float hdrSdrRatio = 1.f) {
+    const auto transfer = dataspace & HAL_DATASPACE_TRANSFER_MASK;
+    const auto range = dataspace & HAL_DATASPACE_RANGE_MASK;
+
+    if (transfer == HAL_DATASPACE_TRANSFER_ST2084 || transfer == HAL_DATASPACE_TRANSFER_HLG) {
+        return HdrRenderType::GENERIC_HDR;
+    }
+
+    static const auto BT2020_LINEAR_EXT = static_cast<ui::Dataspace>(HAL_DATASPACE_STANDARD_BT2020 |
+                                                                     HAL_DATASPACE_TRANSFER_LINEAR |
+                                                                     HAL_DATASPACE_RANGE_EXTENDED);
+
+    if ((dataspace == BT2020_LINEAR_EXT || dataspace == ui::Dataspace::V0_SCRGB) &&
+        pixelFormat.has_value() && pixelFormat.value() == ui::PixelFormat::RGBA_FP16) {
+        return HdrRenderType::GENERIC_HDR;
+    }
+
+    // Extended range layer with an hdr/sdr ratio of > 1.01f can "self-promote" to HDR.
+    if (range == HAL_DATASPACE_RANGE_EXTENDED && hdrSdrRatio > 1.01f) {
+        return HdrRenderType::DISPLAY_HDR;
+    }
+
+    return HdrRenderType::SDR;
+}
+
+} // namespace android
\ No newline at end of file
diff --git a/libs/ui/tests/Android.bp b/libs/ui/tests/Android.bp
index 831b64d..8ce017d 100644
--- a/libs/ui/tests/Android.bp
+++ b/libs/ui/tests/Android.bp
@@ -164,9 +164,9 @@
 }
 
 cc_test {
-    name: "DataspaceUtils_test",
+    name: "HdrRenderTypeUtils_test",
     shared_libs: ["libui"],
-    srcs: ["DataspaceUtils_test.cpp"],
+    srcs: ["HdrRenderTypeUtils_test.cpp"],
     cflags: [
         "-Wall",
         "-Werror",
diff --git a/libs/ui/tests/DataspaceUtils_test.cpp b/libs/ui/tests/DataspaceUtils_test.cpp
deleted file mode 100644
index 3e09671..0000000
--- a/libs/ui/tests/DataspaceUtils_test.cpp
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#undef LOG_TAG
-#define LOG_TAG "DataspaceUtilsTest"
-
-#include <gtest/gtest.h>
-#include <ui/DataspaceUtils.h>
-
-namespace android {
-
-class DataspaceUtilsTest : public testing::Test {};
-
-TEST_F(DataspaceUtilsTest, isHdrDataspace) {
-    EXPECT_TRUE(isHdrDataspace(ui::Dataspace::BT2020_ITU_HLG));
-    EXPECT_TRUE(isHdrDataspace(ui::Dataspace::BT2020_ITU_PQ));
-    EXPECT_TRUE(isHdrDataspace(ui::Dataspace::BT2020_PQ));
-    EXPECT_TRUE(isHdrDataspace(ui::Dataspace::BT2020_HLG));
-
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::V0_SRGB_LINEAR));
-    // scRGB defines a very wide gamut but not an expanded luminance range
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::V0_SCRGB_LINEAR));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::V0_SRGB));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::V0_SCRGB));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::V0_JFIF));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::V0_BT601_625));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::V0_BT601_525));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::V0_BT709));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::DCI_P3_LINEAR));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::DCI_P3));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::DISPLAY_P3_LINEAR));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::DISPLAY_P3));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::ADOBE_RGB));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::BT2020_LINEAR));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::BT2020));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::BT2020_ITU));
-    EXPECT_FALSE(isHdrDataspace(ui::Dataspace::DISPLAY_BT2020));
-}
-
-} // namespace android
diff --git a/libs/ui/tests/HdrRenderTypeUtils_test.cpp b/libs/ui/tests/HdrRenderTypeUtils_test.cpp
new file mode 100644
index 0000000..efe819d
--- /dev/null
+++ b/libs/ui/tests/HdrRenderTypeUtils_test.cpp
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "HdrRenderTypeUtilsTest"
+
+#include <gtest/gtest.h>
+#include <ui/HdrRenderTypeUtils.h>
+
+namespace android {
+
+class HdrRenderTypeUtilsTest : public testing::Test {};
+
+TEST_F(HdrRenderTypeUtilsTest, getHdrRenderType) {
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::BT2020_ITU_HLG, std::nullopt),
+              HdrRenderType::GENERIC_HDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::BT2020_ITU_PQ, std::nullopt),
+              HdrRenderType::GENERIC_HDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::BT2020_PQ, std::nullopt), HdrRenderType::GENERIC_HDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::BT2020_HLG, std::nullopt),
+              HdrRenderType::GENERIC_HDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_SCRGB,
+                               std::optional<ui::PixelFormat>(ui::PixelFormat::RGBA_FP16)),
+              HdrRenderType::GENERIC_HDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_SCRGB,
+                               std::optional<ui::PixelFormat>(ui::PixelFormat::RGBA_8888), 2.f),
+              HdrRenderType::DISPLAY_HDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_SCRGB_LINEAR,
+                               std::optional<ui::PixelFormat>(ui::PixelFormat::RGBA_8888), 2.f),
+              HdrRenderType::DISPLAY_HDR);
+
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_SRGB_LINEAR, std::nullopt), HdrRenderType::SDR);
+    // scRGB defines a very wide gamut but not an expanded luminance range
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_SCRGB_LINEAR, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_SCRGB, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_SRGB, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_JFIF, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_BT601_625, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_BT601_525, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::V0_BT709, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::DCI_P3_LINEAR, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::DCI_P3, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::DISPLAY_P3_LINEAR, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::DISPLAY_P3, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::ADOBE_RGB, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::BT2020_LINEAR, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::BT2020, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::BT2020_ITU, std::nullopt), HdrRenderType::SDR);
+    EXPECT_EQ(getHdrRenderType(ui::Dataspace::DISPLAY_BT2020, std::nullopt), HdrRenderType::SDR);
+}
+
+} // namespace android
diff --git a/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp b/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp
index bf9b031..2d59e8b 100644
--- a/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp
+++ b/libs/ultrahdr/fuzzer/ultrahdr_enc_fuzzer.cpp
@@ -161,10 +161,8 @@
                 fillP010Buffer(bufferUVHdr.get(), width, height / 2, uvStride);
             }
         } else {
-            size_t map_width = static_cast<size_t>(
-                    floor((width + kMapDimensionScaleFactor - 1) / kMapDimensionScaleFactor));
-            size_t map_height = static_cast<size_t>(
-                    floor((height + kMapDimensionScaleFactor - 1) / kMapDimensionScaleFactor));
+            size_t map_width = width / kMapDimensionScaleFactor;
+            size_t map_height = height / kMapDimensionScaleFactor;
             // init 400 image
             grayImg.width = map_width;
             grayImg.height = map_height;
@@ -207,7 +205,7 @@
                 fill420Buffer(bufferYSdr.get(), width, height, yStride);
                 size_t yuv420UVSize = uvStride * yuv420Img.height / 2 * 2;
                 bufferUVSdr = std::make_unique<uint8_t[]>(yuv420UVSize);
-                yuv420Img.chroma_data = bufferYSdr.get();
+                yuv420Img.chroma_data = bufferUVSdr.get();
                 yuv420Img.chroma_stride = uvStride;
                 fill420Buffer(bufferUVSdr.get(), width / 2, height / 2, uvStride);
                 fill420Buffer(bufferUVSdr.get() + uvStride * height / 2, width / 2, height / 2,
diff --git a/libs/ultrahdr/include/ultrahdr/jpegdecoderhelper.h b/libs/ultrahdr/include/ultrahdr/jpegdecoderhelper.h
index 8b5499a..30149c1 100644
--- a/libs/ultrahdr/include/ultrahdr/jpegdecoderhelper.h
+++ b/libs/ultrahdr/include/ultrahdr/jpegdecoderhelper.h
@@ -26,6 +26,8 @@
 #include <utils/Errors.h>
 #include <vector>
 
+// constraint on max width and max height is only due to device alloc constraints
+// Can tune these values basing on the target device
 static const int kMaxWidth = 8192;
 static const int kMaxHeight = 8192;
 
@@ -74,15 +76,27 @@
      */
     size_t getXMPSize();
     /*
+     * Extracts EXIF package and updates the EXIF position / length without decoding the image.
+     */
+    bool extractEXIF(const void* image, int length);
+    /*
      * Returns the EXIF data from the image.
+     * This method must be called after extractEXIF() or decompressImage().
      */
     void* getEXIFPtr();
     /*
      * Returns the decompressed EXIF buffer size. This method must be called only after
-     * calling decompressImage() or getCompressedImageParameters().
+     * calling decompressImage(), extractEXIF() or getCompressedImageParameters().
      */
     size_t getEXIFSize();
     /*
+     * Returns the position offset of EXIF package
+     * (4 bypes offset to FF sign, the byte after FF E1 XX XX <this byte>),
+     * or -1  if no EXIF exists.
+     * This method must be called after extractEXIF() or decompressImage().
+     */
+    int getEXIFPos() { return mExifPos; }
+    /*
      * Returns the ICC data from the image.
      */
     void* getICCPtr();
@@ -94,9 +108,8 @@
     /*
      * Decompresses metadata of the image. All vectors are owned by the caller.
      */
-    bool getCompressedImageParameters(const void* image, int length,
-                                      size_t* pWidth, size_t* pHeight,
-                                      std::vector<uint8_t>* iccData,
+    bool getCompressedImageParameters(const void* image, int length, size_t* pWidth,
+                                      size_t* pHeight, std::vector<uint8_t>* iccData,
                                       std::vector<uint8_t>* exifData);
 
 private:
@@ -121,6 +134,9 @@
     // Resolution of the decompressed image.
     size_t mWidth;
     size_t mHeight;
+
+    // Position of EXIF package, default value is -1 which means no EXIF package appears.
+    size_t mExifPos;
 };
 } /* namespace android::ultrahdr  */
 
diff --git a/libs/ultrahdr/include/ultrahdr/jpegr.h b/libs/ultrahdr/include/ultrahdr/jpegr.h
index 850cb32..114c81d 100644
--- a/libs/ultrahdr/include/ultrahdr/jpegr.h
+++ b/libs/ultrahdr/include/ultrahdr/jpegr.h
@@ -366,22 +366,24 @@
      * the compressed gain map and optionally the exif package as inputs, and generate the XMP
      * metadata, and finally append everything in the order of:
      *     SOI, APP2(EXIF) (if EXIF is from outside), APP2(XMP), primary image, gain map
-     * Note that EXIF package is only available for encoding API-0 and API-1. For encoding API-2 and
-     * API-3 this parameter is null, but the primary image in JPEG/R may still have EXIF as long as
-     * the input JPEG has EXIF.
      *
-
+     * Note that in the final JPEG/R output, EXIF package will appear if ONLY ONE of the following
+     * conditions is fulfilled:
+     *  (1) EXIF package is available from outside input. I.e. pExif != nullptr.
+     *  (2) Input JPEG has EXIF.
+     * If both conditions are fulfilled, this method will return ERROR_JPEGR_INVALID_INPUT_TYPE
+     *
      * @param primary_jpg_image_ptr destination of primary image
      * @param gainmap_jpg_image_ptr destination of compressed gain map image
-     * @param (nullable) exif EXIF package
-     * @param (nullable) icc ICC package
+     * @param (nullable) pExif EXIF package
+     * @param (nullable) pIcc ICC package
      * @param icc_size length in bytes of ICC package
      * @param metadata JPEG/R metadata to encode in XMP of the jpeg
      * @param dest compressed JPEGR image
      * @return NO_ERROR if calculation succeeds, error code if error occurs.
      */
     status_t appendGainMap(jr_compressed_ptr primary_jpg_image_ptr,
-                           jr_compressed_ptr gainmap_jpg_image_ptr, jr_exif_ptr exif, void* icc,
+                           jr_compressed_ptr gainmap_jpg_image_ptr, jr_exif_ptr pExif, void* pIcc,
                            size_t icc_size, ultrahdr_metadata_ptr metadata, jr_compressed_ptr dest);
 
     /*
diff --git a/libs/ultrahdr/include/ultrahdr/ultrahdr.h b/libs/ultrahdr/include/ultrahdr/ultrahdr.h
index 66f7088..0252391 100644
--- a/libs/ultrahdr/include/ultrahdr/ultrahdr.h
+++ b/libs/ultrahdr/include/ultrahdr/ultrahdr.h
@@ -30,6 +30,7 @@
 } ultrahdr_color_gamut;
 
 // Transfer functions for image data
+// TODO: TF LINEAR is deprecated, remove this enum and the code surrounding it.
 typedef enum {
   ULTRAHDR_TF_UNSPECIFIED = -1,
   ULTRAHDR_TF_LINEAR = 0,
diff --git a/libs/ultrahdr/jpegdecoderhelper.cpp b/libs/ultrahdr/jpegdecoderhelper.cpp
index 33bf9ef..2e7940c 100644
--- a/libs/ultrahdr/jpegdecoderhelper.cpp
+++ b/libs/ultrahdr/jpegdecoderhelper.cpp
@@ -26,18 +26,23 @@
 
 namespace android::ultrahdr {
 
-#define ALIGNM(x, m)  ((((x) + ((m) - 1)) / (m)) * (m))
+#define ALIGNM(x, m) ((((x) + ((m)-1)) / (m)) * (m))
 
-const uint32_t kAPP0Marker = JPEG_APP0;      // JFIF
-const uint32_t kAPP1Marker = JPEG_APP0 + 1;  // EXIF, XMP
-const uint32_t kAPP2Marker = JPEG_APP0 + 2;  // ICC
+const uint32_t kAPP0Marker = JPEG_APP0;     // JFIF
+const uint32_t kAPP1Marker = JPEG_APP0 + 1; // EXIF, XMP
+const uint32_t kAPP2Marker = JPEG_APP0 + 2; // ICC
 
-const std::string kXmpNameSpace = "http://ns.adobe.com/xap/1.0/";
-const std::string kExifIdCode = "Exif";
 constexpr uint32_t kICCMarkerHeaderSize = 14;
 constexpr uint8_t kICCSig[] = {
         'I', 'C', 'C', '_', 'P', 'R', 'O', 'F', 'I', 'L', 'E', '\0',
 };
+constexpr uint8_t kXmpNameSpace[] = {
+        'h', 't', 't', 'p', ':', '/', '/', 'n', 's', '.', 'a', 'd', 'o', 'b', 'e',
+        '.', 'c', 'o', 'm', '/', 'x', 'a', 'p', '/', '1', '.', '0', '/', '\0',
+};
+constexpr uint8_t kExifIdCode[] = {
+        'E', 'x', 'i', 'f', '\0', '\0',
+};
 
 struct jpegr_source_mgr : jpeg_source_mgr {
     jpegr_source_mgr(const uint8_t* ptr, int len);
@@ -76,8 +81,8 @@
 
 static void jpegr_term_source(j_decompress_ptr /*cinfo*/) {}
 
-jpegr_source_mgr::jpegr_source_mgr(const uint8_t* ptr, int len) :
-        mBufferPtr(ptr), mBufferLength(len) {
+jpegr_source_mgr::jpegr_source_mgr(const uint8_t* ptr, int len)
+      : mBufferPtr(ptr), mBufferLength(len) {
     init_source = jpegr_init_source;
     fill_input_buffer = jpegr_fill_input_buffer;
     skip_input_data = jpegr_skip_input_data;
@@ -92,25 +97,18 @@
     longjmp(err->setjmp_buffer, 1);
 }
 
-JpegDecoderHelper::JpegDecoderHelper() {
-}
+JpegDecoderHelper::JpegDecoderHelper() {}
 
-JpegDecoderHelper::~JpegDecoderHelper() {
-}
+JpegDecoderHelper::~JpegDecoderHelper() {}
 
 bool JpegDecoderHelper::decompressImage(const void* image, int length, bool decodeToRGBA) {
     if (image == nullptr || length <= 0) {
         ALOGE("Image size can not be handled: %d", length);
         return false;
     }
-
     mResultBuffer.clear();
     mXMPBuffer.clear();
-    if (!decode(image, length, decodeToRGBA)) {
-        return false;
-    }
-
-    return true;
+    return decode(image, length, decodeToRGBA);
 }
 
 void* JpegDecoderHelper::getDecompressedImagePtr() {
@@ -153,11 +151,15 @@
     return mHeight;
 }
 
-bool JpegDecoderHelper::decode(const void* image, int length, bool decodeToRGBA) {
+// Here we only handle the first EXIF package, and in theary EXIF (or JFIF) must be the first
+// in the image file.
+// We assume that all packages are starting with two bytes marker (eg FF E1 for EXIF package),
+// two bytes of package length which is stored in marker->original_length, and the real data
+// which is stored in marker->data.
+bool JpegDecoderHelper::extractEXIF(const void* image, int length) {
     jpeg_decompress_struct cinfo;
     jpegr_source_mgr mgr(static_cast<const uint8_t*>(image), length);
     jpegrerror_mgr myerr;
-    bool status = true;
 
     cinfo.err = jpeg_std_error(&myerr.pub);
     myerr.pub.error_exit = jpegrerror_exit;
@@ -170,11 +172,61 @@
 
     jpeg_save_markers(&cinfo, kAPP0Marker, 0xFFFF);
     jpeg_save_markers(&cinfo, kAPP1Marker, 0xFFFF);
-    jpeg_save_markers(&cinfo, kAPP2Marker, 0xFFFF);
 
     cinfo.src = &mgr;
     jpeg_read_header(&cinfo, TRUE);
 
+    size_t pos = 2;  // position after SOI
+    for (jpeg_marker_struct* marker = cinfo.marker_list;
+         marker;
+         marker = marker->next) {
+
+        pos += 4;
+        pos += marker->original_length;
+
+        if (marker->marker != kAPP1Marker) {
+            continue;
+        }
+
+        const unsigned int len = marker->data_length;
+
+        if (len > sizeof(kExifIdCode) &&
+            !memcmp(marker->data, kExifIdCode, sizeof(kExifIdCode))) {
+            mEXIFBuffer.resize(len, 0);
+            memcpy(static_cast<void*>(mEXIFBuffer.data()), marker->data, len);
+            mExifPos = pos - marker->original_length;
+            break;
+        }
+    }
+
+    jpeg_destroy_decompress(&cinfo);
+    return true;
+}
+
+bool JpegDecoderHelper::decode(const void* image, int length, bool decodeToRGBA) {
+    bool status = true;
+    jpeg_decompress_struct cinfo;
+    jpegrerror_mgr myerr;
+    cinfo.err = jpeg_std_error(&myerr.pub);
+    myerr.pub.error_exit = jpegrerror_exit;
+    if (setjmp(myerr.setjmp_buffer)) {
+        jpeg_destroy_decompress(&cinfo);
+        return false;
+    }
+
+    jpeg_create_decompress(&cinfo);
+
+    jpeg_save_markers(&cinfo, kAPP0Marker, 0xFFFF);
+    jpeg_save_markers(&cinfo, kAPP1Marker, 0xFFFF);
+    jpeg_save_markers(&cinfo, kAPP2Marker, 0xFFFF);
+
+    jpegr_source_mgr mgr(static_cast<const uint8_t*>(image), length);
+    cinfo.src = &mgr;
+    if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
+        jpeg_destroy_decompress(&cinfo);
+        return false;
+    }
+
     // Save XMP data, EXIF data, and ICC data.
     // Here we only handle the first XMP / EXIF / ICC package.
     // We assume that all packages are starting with two bytes marker (eg FF E1 for EXIF package),
@@ -183,30 +235,29 @@
     bool exifAppears = false;
     bool xmpAppears = false;
     bool iccAppears = false;
+    size_t pos = 2;  // position after SOI
     for (jpeg_marker_struct* marker = cinfo.marker_list;
          marker && !(exifAppears && xmpAppears && iccAppears);
          marker = marker->next) {
-
+         pos += 4;
+         pos += marker->original_length;
         if (marker->marker != kAPP1Marker && marker->marker != kAPP2Marker) {
             continue;
         }
         const unsigned int len = marker->data_length;
         if (!xmpAppears &&
-            len > kXmpNameSpace.size() &&
-            !strncmp(reinterpret_cast<const char*>(marker->data),
-                     kXmpNameSpace.c_str(),
-                     kXmpNameSpace.size())) {
+            len > sizeof(kXmpNameSpace) &&
+            !memcmp(marker->data, kXmpNameSpace, sizeof(kXmpNameSpace))) {
             mXMPBuffer.resize(len+1, 0);
             memcpy(static_cast<void*>(mXMPBuffer.data()), marker->data, len);
             xmpAppears = true;
         } else if (!exifAppears &&
-                   len > kExifIdCode.size() &&
-                   !strncmp(reinterpret_cast<const char*>(marker->data),
-                            kExifIdCode.c_str(),
-                            kExifIdCode.size())) {
+                   len > sizeof(kExifIdCode) &&
+                   !memcmp(marker->data, kExifIdCode, sizeof(kExifIdCode))) {
             mEXIFBuffer.resize(len, 0);
             memcpy(static_cast<void*>(mEXIFBuffer.data()), marker->data, len);
             exifAppears = true;
+            mExifPos = pos - marker->original_length;
         } else if (!iccAppears &&
                    len > sizeof(kICCSig) &&
                    !memcmp(marker->data, kICCSig, sizeof(kICCSig))) {
@@ -216,31 +267,25 @@
         }
     }
 
-    if (cinfo.image_width > kMaxWidth || cinfo.image_height > kMaxHeight) {
-        // constraint on max width and max height is only due to alloc constraints
-        // tune these values basing on the target device
+    mWidth = cinfo.image_width;
+    mHeight = cinfo.image_height;
+    if (mWidth > kMaxWidth || mHeight > kMaxHeight) {
         status = false;
         goto CleanUp;
     }
 
-    mWidth = cinfo.image_width;
-    mHeight = cinfo.image_height;
-
     if (decodeToRGBA) {
         // The primary image is expected to be yuv420 sampling
-            if (cinfo.jpeg_color_space != JCS_YCbCr) {
-                status = false;
-                ALOGE("%s: decodeToRGBA unexpected jpeg color space ", __func__);
-                goto CleanUp;
-            }
-            if (cinfo.comp_info[0].h_samp_factor != 2 ||
-                    cinfo.comp_info[1].h_samp_factor != 1 ||
-                    cinfo.comp_info[2].h_samp_factor != 1 ||
-                    cinfo.comp_info[0].v_samp_factor != 2 ||
-                    cinfo.comp_info[1].v_samp_factor != 1 ||
-                    cinfo.comp_info[2].v_samp_factor != 1 ) {
-                status = false;
-                ALOGE("%s: decodeToRGBA unexpected primary image sub-sampling", __func__);
+        if (cinfo.jpeg_color_space != JCS_YCbCr) {
+            status = false;
+            ALOGE("%s: decodeToRGBA unexpected jpeg color space ", __func__);
+            goto CleanUp;
+        }
+        if (cinfo.comp_info[0].h_samp_factor != 2 || cinfo.comp_info[0].v_samp_factor != 2 ||
+            cinfo.comp_info[1].h_samp_factor != 1 || cinfo.comp_info[1].v_samp_factor != 1 ||
+            cinfo.comp_info[2].h_samp_factor != 1 || cinfo.comp_info[2].v_samp_factor != 1) {
+            status = false;
+            ALOGE("%s: decodeToRGBA unexpected primary image sub-sampling", __func__);
             goto CleanUp;
         }
         // 4 bytes per pixel
@@ -248,12 +293,9 @@
         cinfo.out_color_space = JCS_EXT_RGBA;
     } else {
         if (cinfo.jpeg_color_space == JCS_YCbCr) {
-            if (cinfo.comp_info[0].h_samp_factor != 2 ||
-                cinfo.comp_info[1].h_samp_factor != 1 ||
-                cinfo.comp_info[2].h_samp_factor != 1 ||
-                cinfo.comp_info[0].v_samp_factor != 2 ||
-                cinfo.comp_info[1].v_samp_factor != 1 ||
-                cinfo.comp_info[2].v_samp_factor != 1) {
+            if (cinfo.comp_info[0].h_samp_factor != 2 || cinfo.comp_info[0].v_samp_factor != 2 ||
+                cinfo.comp_info[1].h_samp_factor != 1 || cinfo.comp_info[1].v_samp_factor != 1 ||
+                cinfo.comp_info[2].h_samp_factor != 1 || cinfo.comp_info[2].v_samp_factor != 1) {
                 status = false;
                 ALOGE("%s: decoding to YUV only supports 4:2:0 subsampling", __func__);
                 goto CleanUp;
@@ -271,11 +313,9 @@
     }
 
     cinfo.dct_method = JDCT_ISLOW;
-
     jpeg_start_decompress(&cinfo);
-
     if (!decompress(&cinfo, static_cast<const uint8_t*>(mResultBuffer.data()),
-            cinfo.jpeg_color_space == JCS_GRAYSCALE)) {
+                    cinfo.jpeg_color_space == JCS_GRAYSCALE)) {
         status = false;
         goto CleanUp;
     }
@@ -288,25 +328,20 @@
 }
 
 bool JpegDecoderHelper::decompress(jpeg_decompress_struct* cinfo, const uint8_t* dest,
-        bool isSingleChannel) {
-    if (isSingleChannel) {
-        return decompressSingleChannel(cinfo, dest);
-    }
-    if (cinfo->out_color_space == JCS_EXT_RGBA)
-        return decompressRGBA(cinfo, dest);
-    else
-        return decompressYUV(cinfo, dest);
+                                   bool isSingleChannel) {
+    return isSingleChannel
+            ? decompressSingleChannel(cinfo, dest)
+            : ((cinfo->out_color_space == JCS_EXT_RGBA) ? decompressRGBA(cinfo, dest)
+                                                        : decompressYUV(cinfo, dest));
 }
 
-bool JpegDecoderHelper::getCompressedImageParameters(const void* image, int length,
-                              size_t *pWidth, size_t *pHeight,
-                              std::vector<uint8_t> *iccData , std::vector<uint8_t> *exifData) {
+bool JpegDecoderHelper::getCompressedImageParameters(const void* image, int length, size_t* pWidth,
+                                                     size_t* pHeight, std::vector<uint8_t>* iccData,
+                                                     std::vector<uint8_t>* exifData) {
     jpeg_decompress_struct cinfo;
-    jpegr_source_mgr mgr(static_cast<const uint8_t*>(image), length);
     jpegrerror_mgr myerr;
     cinfo.err = jpeg_std_error(&myerr.pub);
     myerr.pub.error_exit = jpegrerror_exit;
-
     if (setjmp(myerr.setjmp_buffer)) {
         jpeg_destroy_decompress(&cinfo);
         return false;
@@ -316,6 +351,7 @@
     jpeg_save_markers(&cinfo, kAPP1Marker, 0xFFFF);
     jpeg_save_markers(&cinfo, kAPP2Marker, 0xFFFF);
 
+    jpegr_source_mgr mgr(static_cast<const uint8_t*>(image), length);
     cinfo.src = &mgr;
     if (jpeg_read_header(&cinfo, TRUE) != JPEG_HEADER_OK) {
         jpeg_destroy_decompress(&cinfo);
@@ -330,8 +366,7 @@
     }
 
     if (iccData != nullptr) {
-        for (jpeg_marker_struct* marker = cinfo.marker_list; marker;
-             marker = marker->next) {
+        for (jpeg_marker_struct* marker = cinfo.marker_list; marker; marker = marker->next) {
             if (marker->marker != kAPP2Marker) {
                 continue;
             }
@@ -353,9 +388,8 @@
             }
 
             const unsigned int len = marker->data_length;
-            if (len >= kExifIdCode.size() &&
-                !strncmp(reinterpret_cast<const char*>(marker->data), kExifIdCode.c_str(),
-                         kExifIdCode.size())) {
+            if (len >= sizeof(kExifIdCode) &&
+                !memcmp(marker->data, kExifIdCode, sizeof(kExifIdCode))) {
                 exifData->resize(len, 0);
                 memcpy(static_cast<void*>(exifData->data()), marker->data, len);
                 exifAppears = true;
@@ -368,25 +402,20 @@
 }
 
 bool JpegDecoderHelper::decompressRGBA(jpeg_decompress_struct* cinfo, const uint8_t* dest) {
-    JSAMPLE* decodeDst = (JSAMPLE*) dest;
-    uint32_t lines = 0;
-    // TODO: use batches for more effectiveness
-    while (lines < cinfo->image_height) {
-        uint32_t ret = jpeg_read_scanlines(cinfo, &decodeDst, 1);
-        if (ret == 0) {
-            break;
-        }
-        decodeDst += cinfo->image_width * 4;
-        lines++;
+    JSAMPLE* out = (JSAMPLE*)dest;
+
+    while (cinfo->output_scanline < cinfo->image_height) {
+        if (1 != jpeg_read_scanlines(cinfo, &out, 1)) return false;
+        out += cinfo->image_width * 4;
     }
-    return lines == cinfo->image_height;
+    return true;
 }
 
 bool JpegDecoderHelper::decompressYUV(jpeg_decompress_struct* cinfo, const uint8_t* dest) {
     JSAMPROW y[kCompressBatchSize];
     JSAMPROW cb[kCompressBatchSize / 2];
     JSAMPROW cr[kCompressBatchSize / 2];
-    JSAMPARRAY planes[3] {y, cb, cr};
+    JSAMPARRAY planes[3]{y, cb, cr};
 
     size_t y_plane_size = cinfo->image_width * cinfo->image_height;
     size_t uv_plane_size = y_plane_size / 4;
@@ -405,7 +434,7 @@
     JSAMPROW y_intrm[kCompressBatchSize];
     JSAMPROW cb_intrm[kCompressBatchSize / 2];
     JSAMPROW cr_intrm[kCompressBatchSize / 2];
-    JSAMPARRAY planes_intrm[3] {y_intrm, cb_intrm, cr_intrm};
+    JSAMPARRAY planes_intrm[3]{y_intrm, cb_intrm, cr_intrm};
     if (!is_width_aligned) {
         size_t mcu_row_size = aligned_width * kCompressBatchSize * 3 / 2;
         buffer_intrm = std::make_unique<uint8_t[]>(mcu_row_size);
@@ -462,9 +491,10 @@
     return true;
 }
 
-bool JpegDecoderHelper::decompressSingleChannel(jpeg_decompress_struct* cinfo, const uint8_t* dest) {
+bool JpegDecoderHelper::decompressSingleChannel(jpeg_decompress_struct* cinfo,
+                                                const uint8_t* dest) {
     JSAMPROW y[kCompressBatchSize];
-    JSAMPARRAY planes[1] {y};
+    JSAMPARRAY planes[1]{y};
 
     uint8_t* y_plane = const_cast<uint8_t*>(dest);
     std::unique_ptr<uint8_t[]> empty = std::make_unique<uint8_t[]>(cinfo->image_width);
@@ -475,7 +505,7 @@
     std::unique_ptr<uint8_t[]> buffer_intrm = nullptr;
     uint8_t* y_plane_intrm = nullptr;
     JSAMPROW y_intrm[kCompressBatchSize];
-    JSAMPARRAY planes_intrm[1] {y_intrm};
+    JSAMPARRAY planes_intrm[1]{y_intrm};
     if (!is_width_aligned) {
         size_t mcu_row_size = aligned_width * kCompressBatchSize;
         buffer_intrm = std::make_unique<uint8_t[]>(mcu_row_size);
@@ -510,4 +540,4 @@
     return true;
 }
 
-} // namespace ultrahdr
+} // namespace android::ultrahdr
diff --git a/libs/ultrahdr/jpegr.cpp b/libs/ultrahdr/jpegr.cpp
index dc439d7..74760d9 100644
--- a/libs/ultrahdr/jpegr.cpp
+++ b/libs/ultrahdr/jpegr.cpp
@@ -72,6 +72,32 @@
   return cpuCoreCount;
 }
 
+/*
+ * Helper function copies the JPEG image from without EXIF.
+ *
+ * @param pDest destination of the data to be written.
+ * @param pSource source of data being written.
+ * @param exif_pos position of the EXIF package, which is aligned with jpegdecoder.getEXIFPos().
+ *                 (4 bytes offset to FF sign, the byte after FF E1 XX XX <this byte>).
+ * @param exif_size exif size without the initial 4 bytes, aligned with jpegdecoder.getEXIFSize().
+ */
+static void copyJpegWithoutExif(jr_compressed_ptr pDest,
+                                jr_compressed_ptr pSource,
+                                size_t exif_pos,
+                                size_t exif_size) {
+  memcpy(pDest, pSource, sizeof(jpegr_compressed_struct));
+
+  const size_t exif_offset = 4; //exif_pos has 4 bytes offset to the FF sign
+  pDest->length = pSource->length - exif_size - exif_offset;
+  pDest->data = new uint8_t[pDest->length];
+  std::unique_ptr<uint8_t[]> dest_data;
+  dest_data.reset(reinterpret_cast<uint8_t*>(pDest->data));
+  memcpy(pDest->data, pSource->data, exif_pos - exif_offset);
+  memcpy((uint8_t*)pDest->data + exif_pos - exif_offset,
+         (uint8_t*)pSource->data + exif_pos + exif_size,
+         pSource->length - exif_pos - exif_size);
+}
+
 status_t JpegR::areInputArgumentsValid(jr_uncompressed_ptr p010_image_ptr,
                                        jr_uncompressed_ptr yuv420_image_ptr,
                                        ultrahdr_transfer_function hdr_tf,
@@ -801,10 +827,8 @@
 
   size_t image_width = yuv420_image_ptr->width;
   size_t image_height = yuv420_image_ptr->height;
-  size_t map_width = static_cast<size_t>(
-          floor((image_width + kMapDimensionScaleFactor - 1) / kMapDimensionScaleFactor));
-  size_t map_height = static_cast<size_t>(
-          floor((image_height + kMapDimensionScaleFactor - 1) / kMapDimensionScaleFactor));
+  size_t map_width = image_width / kMapDimensionScaleFactor;
+  size_t map_height = image_height / kMapDimensionScaleFactor;
 
   dest->data = new uint8_t[map_width * map_height];
   dest->width = map_width;
@@ -817,10 +841,13 @@
   map_data.reset(reinterpret_cast<uint8_t*>(dest->data));
 
   ColorTransformFn hdrInvOetf = nullptr;
-  float hdr_white_nits = kSdrWhiteNits;
+  float hdr_white_nits;
   switch (hdr_tf) {
     case ULTRAHDR_TF_LINEAR:
       hdrInvOetf = identityConversion;
+      // Note: this will produce clipping if the input exceeds kHlgMaxNits.
+      // TODO: TF LINEAR will be deprecated.
+      hdr_white_nits = kHlgMaxNits;
       break;
     case ULTRAHDR_TF_HLG:
 #if USE_HLG_INVOETF_LUT
@@ -984,10 +1011,8 @@
   // TODO: remove once map scaling factor is computed based on actual map dims
   size_t image_width = yuv420_image_ptr->width;
   size_t image_height = yuv420_image_ptr->height;
-  size_t map_width = static_cast<size_t>(
-          floor((image_width + kMapDimensionScaleFactor - 1) / kMapDimensionScaleFactor));
-  size_t map_height = static_cast<size_t>(
-          floor((image_height + kMapDimensionScaleFactor - 1) / kMapDimensionScaleFactor));
+  size_t map_width = image_width / kMapDimensionScaleFactor;
+  size_t map_height = image_height / kMapDimensionScaleFactor;
   if (map_width != gainmap_image_ptr->width || map_height != gainmap_image_ptr->height) {
     ALOGE("gain map dimensions and primary image dimensions are not to scale, computed gain map "
           "resolution is %dx%d, received gain map resolution is %dx%d",
@@ -1153,7 +1178,8 @@
 // JPEG/R structure:
 // SOI (ff d8)
 //
-// (Optional, only if EXIF package is from outside)
+// (Optional, if EXIF package is from outside (Encode API-0 API-1), or if EXIF package presents
+// in the JPEG input (Encode API-2, API-3, API-4))
 // APP1 (ff e1)
 // 2 bytes of length (2 + length of exif package)
 // EXIF package (this includes the first two bytes representing the package length)
@@ -1167,7 +1193,7 @@
 // 2 bytes of length
 // MPF
 //
-// (Required) primary image (without the first two bytes (SOI), may have other packages)
+// (Required) primary image (without the first two bytes (SOI) and EXIF, may have other packages)
 //
 // SOI (ff d8)
 //
@@ -1184,8 +1210,8 @@
 // Adobe XMP spec part 3 for XMP marker
 // ICC v4.3 spec for ICC
 status_t JpegR::appendGainMap(jr_compressed_ptr primary_jpg_image_ptr,
-                              jr_compressed_ptr gainmap_jpg_image_ptr, jr_exif_ptr exif, void* icc,
-                              size_t icc_size, ultrahdr_metadata_ptr metadata,
+                              jr_compressed_ptr gainmap_jpg_image_ptr, jr_exif_ptr pExif,
+                              void* pIcc, size_t icc_size, ultrahdr_metadata_ptr metadata,
                               jr_compressed_ptr dest) {
   if (primary_jpg_image_ptr == nullptr || gainmap_jpg_image_ptr == nullptr || metadata == nullptr ||
       dest == nullptr) {
@@ -1230,6 +1256,35 @@
   // same as primary
   const int xmp_primary_length = 2 + nameSpaceLength + xmp_primary.size();
 
+  // Check if EXIF package presents in the JPEG input.
+  // If so, extract and remove the EXIF package.
+  JpegDecoderHelper decoder;
+  if (!decoder.extractEXIF(primary_jpg_image_ptr->data, primary_jpg_image_ptr->length)) {
+    return ERROR_JPEGR_DECODE_ERROR;
+  }
+  jpegr_exif_struct exif_from_jpg;
+  exif_from_jpg.data = nullptr;
+  exif_from_jpg.length = 0;
+  jpegr_compressed_struct new_jpg_image;
+  new_jpg_image.data = nullptr;
+  new_jpg_image.length = 0;
+  if (decoder.getEXIFPos() != 0) {
+    if (pExif != nullptr) {
+      ALOGE("received EXIF from outside while the primary image already contains EXIF");
+      return ERROR_JPEGR_INVALID_INPUT_TYPE;
+    }
+    copyJpegWithoutExif(&new_jpg_image,
+                        primary_jpg_image_ptr,
+                        decoder.getEXIFPos(),
+                        decoder.getEXIFSize());
+    exif_from_jpg.data = decoder.getEXIFPtr();
+    exif_from_jpg.length = decoder.getEXIFSize();
+    pExif = &exif_from_jpg;
+  }
+
+  jr_compressed_ptr final_primary_jpg_image_ptr =
+          new_jpg_image.length == 0 ? primary_jpg_image_ptr : &new_jpg_image;
+
   int pos = 0;
   // Begin primary image
   // Write SOI
@@ -1237,15 +1292,15 @@
   JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kSOI, 1, pos));
 
   // Write EXIF
-  if (exif != nullptr) {
-    const int length = 2 + exif->length;
+  if (pExif != nullptr) {
+    const int length = 2 + pExif->length;
     const uint8_t lengthH = ((length >> 8) & 0xff);
     const uint8_t lengthL = (length & 0xff);
     JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kStart, 1, pos));
     JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP1, 1, pos));
     JPEGR_CHECK(Write(dest, &lengthH, 1, pos));
     JPEGR_CHECK(Write(dest, &lengthL, 1, pos));
-    JPEGR_CHECK(Write(dest, exif->data, exif->length, pos));
+    JPEGR_CHECK(Write(dest, pExif->data, pExif->length, pos));
   }
 
   // Prepare and write XMP
@@ -1262,7 +1317,7 @@
   }
 
   // Write ICC
-  if (icc != nullptr && icc_size > 0) {
+  if (pIcc != nullptr && icc_size > 0) {
     const int length = icc_size + 2;
     const uint8_t lengthH = ((length >> 8) & 0xff);
     const uint8_t lengthL = (length & 0xff);
@@ -1270,7 +1325,7 @@
     JPEGR_CHECK(Write(dest, &photos_editing_formats::image_io::JpegMarker::kAPP2, 1, pos));
     JPEGR_CHECK(Write(dest, &lengthH, 1, pos));
     JPEGR_CHECK(Write(dest, &lengthL, 1, pos));
-    JPEGR_CHECK(Write(dest, icc, icc_size, pos));
+    JPEGR_CHECK(Write(dest, pIcc, icc_size, pos));
   }
 
   // Prepare and write MPF
@@ -1278,7 +1333,7 @@
     const int length = 2 + calculateMpfSize();
     const uint8_t lengthH = ((length >> 8) & 0xff);
     const uint8_t lengthL = (length & 0xff);
-    int primary_image_size = pos + length + primary_jpg_image_ptr->length;
+    int primary_image_size = pos + length + final_primary_jpg_image_ptr->length;
     // between APP2 + package size + signature
     // ff e2 00 58 4d 50 46 00
     // 2 + 2 + 4 = 8 (bytes)
@@ -1294,8 +1349,8 @@
   }
 
   // Write primary image
-  JPEGR_CHECK(Write(dest, (uint8_t*)primary_jpg_image_ptr->data + 2,
-                    primary_jpg_image_ptr->length - 2, pos));
+  JPEGR_CHECK(Write(dest, (uint8_t*)final_primary_jpg_image_ptr->data + 2,
+                    final_primary_jpg_image_ptr->length - 2, pos));
   // Finish primary image
 
   // Begin secondary image (gain map)
diff --git a/libs/ultrahdr/tests/Android.bp b/libs/ultrahdr/tests/Android.bp
index 004a582..bda804a 100644
--- a/libs/ultrahdr/tests/Android.bp
+++ b/libs/ultrahdr/tests/Android.bp
@@ -28,6 +28,8 @@
         "gainmapmath_test.cpp",
         "icchelper_test.cpp",
         "jpegr_test.cpp",
+        "jpegencoderhelper_test.cpp",
+        "jpegdecoderhelper_test.cpp",
     ],
     shared_libs: [
         "libimage_io",
@@ -42,38 +44,7 @@
         "libultrahdr",
         "libutils",
     ],
-}
-
-cc_test {
-    name: "jpegencoderhelper_test",
-    test_suites: ["device-tests"],
-    srcs: [
-        "jpegencoderhelper_test.cpp",
-    ],
-    shared_libs: [
-        "libjpeg",
-        "liblog",
-    ],
-    static_libs: [
-        "libgtest",
-        "libjpegencoder",
-    ],
-}
-
-cc_test {
-    name: "jpegdecoderhelper_test",
-    test_suites: ["device-tests"],
-    srcs: [
-        "jpegdecoderhelper_test.cpp",
-    ],
-    shared_libs: [
-        "libjpeg",
-        "liblog",
-    ],
-    static_libs: [
-        "libgtest",
-        "libjpegdecoder",
-        "libultrahdr",
-        "libutils",
+    data: [
+        "./data/*.*",
     ],
 }
diff --git a/libs/ultrahdr/tests/AndroidTest.xml b/libs/ultrahdr/tests/AndroidTest.xml
new file mode 100644
index 0000000..1754a5c
--- /dev/null
+++ b/libs/ultrahdr/tests/AndroidTest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 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.
+-->
+<configuration description="Unit test configuration for ultrahdr_unit_test">
+    <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
+        <option name="cleanup" value="true" />
+        <option name="push-file" key="ultrahdr_unit_test" value="/data/local/tmp/ultrahdr_unit_test" />
+        <option name="push" value="data/*->/data/local/tmp/" />
+    </target_preparer>
+    <test class="com.android.tradefed.testtype.GTest" >
+        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="module-name" value="ultrahdr_unit_test" />
+    </test>
+</configuration>
diff --git a/libs/ultrahdr/tests/jpegdecoderhelper_test.cpp b/libs/ultrahdr/tests/jpegdecoderhelper_test.cpp
index e2da01c..af0d59e 100644
--- a/libs/ultrahdr/tests/jpegdecoderhelper_test.cpp
+++ b/libs/ultrahdr/tests/jpegdecoderhelper_test.cpp
@@ -14,9 +14,9 @@
  * limitations under the License.
  */
 
-#include <ultrahdr/jpegdecoderhelper.h>
-#include <ultrahdr/icc.h>
 #include <gtest/gtest.h>
+#include <ultrahdr/icc.h>
+#include <ultrahdr/jpegdecoderhelper.h>
 #include <utils/Log.h>
 
 #include <fcntl.h>
@@ -24,13 +24,13 @@
 namespace android::ultrahdr {
 
 // No ICC or EXIF
-#define YUV_IMAGE "/sdcard/Documents/minnie-320x240-yuv.jpg"
+#define YUV_IMAGE "/data/local/tmp/minnie-320x240-yuv.jpg"
 #define YUV_IMAGE_SIZE 20193
 // Has ICC and EXIF
-#define YUV_ICC_IMAGE "/sdcard/Documents/minnie-320x240-yuv-icc.jpg"
+#define YUV_ICC_IMAGE "/data/local/tmp/minnie-320x240-yuv-icc.jpg"
 #define YUV_ICC_IMAGE_SIZE 34266
 // No ICC or EXIF
-#define GREY_IMAGE "/sdcard/Documents/minnie-320x240-y.jpg"
+#define GREY_IMAGE "/data/local/tmp/minnie-320x240-y.jpg"
 #define GREY_IMAGE_SIZE 20193
 
 #define IMAGE_WIDTH 320
@@ -44,6 +44,7 @@
     };
     JpegDecoderHelperTest();
     ~JpegDecoderHelperTest();
+
 protected:
     virtual void SetUp();
     virtual void TearDown();
@@ -127,8 +128,8 @@
     std::vector<uint8_t> icc, exif;
 
     JpegDecoderHelper decoder;
-    EXPECT_TRUE(decoder.getCompressedImageParameters(mYuvImage.buffer.get(), mYuvImage.size,
-                                                     &width, &height, &icc, &exif));
+    EXPECT_TRUE(decoder.getCompressedImageParameters(mYuvImage.buffer.get(), mYuvImage.size, &width,
+                                                     &height, &icc, &exif));
 
     EXPECT_EQ(width, IMAGE_WIDTH);
     EXPECT_EQ(height, IMAGE_HEIGHT);
@@ -149,8 +150,7 @@
     EXPECT_GT(icc.size(), 0);
     EXPECT_GT(exif.size(), 0);
 
-    EXPECT_EQ(IccHelper::readIccColorGamut(icc.data(), icc.size()),
-              ULTRAHDR_COLORGAMUT_BT709);
+    EXPECT_EQ(IccHelper::readIccColorGamut(icc.data(), icc.size()), ULTRAHDR_COLORGAMUT_BT709);
 }
 
-}  // namespace android::ultrahdr
+} // namespace android::ultrahdr
diff --git a/libs/ultrahdr/tests/jpegencoderhelper_test.cpp b/libs/ultrahdr/tests/jpegencoderhelper_test.cpp
index 33cb9f6..af54eb2 100644
--- a/libs/ultrahdr/tests/jpegencoderhelper_test.cpp
+++ b/libs/ultrahdr/tests/jpegencoderhelper_test.cpp
@@ -22,13 +22,13 @@
 
 namespace android::ultrahdr {
 
-#define ALIGNED_IMAGE "/sdcard/Documents/minnie-320x240.yu12"
+#define ALIGNED_IMAGE "/data/local/tmp/minnie-320x240.yu12"
 #define ALIGNED_IMAGE_WIDTH 320
 #define ALIGNED_IMAGE_HEIGHT 240
-#define SINGLE_CHANNEL_IMAGE "/sdcard/Documents/minnie-320x240.y"
+#define SINGLE_CHANNEL_IMAGE "/data/local/tmp/minnie-320x240.y"
 #define SINGLE_CHANNEL_IMAGE_WIDTH ALIGNED_IMAGE_WIDTH
 #define SINGLE_CHANNEL_IMAGE_HEIGHT ALIGNED_IMAGE_HEIGHT
-#define UNALIGNED_IMAGE "/sdcard/Documents/minnie-318x240.yu12"
+#define UNALIGNED_IMAGE "/data/local/tmp/minnie-318x240.yu12"
 #define UNALIGNED_IMAGE_WIDTH 318
 #define UNALIGNED_IMAGE_HEIGHT 240
 #define JPEG_QUALITY 90
diff --git a/libs/ultrahdr/tests/jpegr_test.cpp b/libs/ultrahdr/tests/jpegr_test.cpp
index a750867..5fa758e 100644
--- a/libs/ultrahdr/tests/jpegr_test.cpp
+++ b/libs/ultrahdr/tests/jpegr_test.cpp
@@ -30,9 +30,9 @@
 namespace android::ultrahdr {
 
 // resources used by unit tests
-const char* kYCbCrP010FileName = "raw_p010_image.p010";
-const char* kYCbCr420FileName = "raw_yuv420_image.yuv420";
-const char* kSdrJpgFileName = "jpeg_image.jpg";
+const char* kYCbCrP010FileName = "/data/local/tmp/raw_p010_image.p010";
+const char* kYCbCr420FileName = "/data/local/tmp/raw_yuv420_image.yuv420";
+const char* kSdrJpgFileName = "/data/local/tmp/jpeg_image.jpg";
 const int kImageWidth = 1280;
 const int kImageHeight = 720;
 const int kQuality = 90;
diff --git a/services/gpuservice/GpuService.cpp b/services/gpuservice/GpuService.cpp
index 4a08c11..48d793a 100644
--- a/services/gpuservice/GpuService.cpp
+++ b/services/gpuservice/GpuService.cpp
@@ -143,7 +143,7 @@
 
     ALOGV("shellCommand");
     for (size_t i = 0, n = args.size(); i < n; i++)
-        ALOGV("  arg[%zu]: '%s'", i, String8(args[i]).string());
+        ALOGV("  arg[%zu]: '%s'", i, String8(args[i]).c_str());
 
     if (args.size() >= 1) {
         if (args[0] == String16("vkjson")) return cmdVkjson(out, err);
diff --git a/services/inputflinger/Android.bp b/services/inputflinger/Android.bp
index 18f6dbc..7451037 100644
--- a/services/inputflinger/Android.bp
+++ b/services/inputflinger/Android.bp
@@ -77,6 +77,7 @@
         "InputCommonConverter.cpp",
         "InputDeviceMetricsCollector.cpp",
         "InputProcessor.cpp",
+        "PointerChoreographer.cpp",
         "PreferStylusOverTouchBlocker.cpp",
         "UnwantedInteractionBlocker.cpp",
     ],
@@ -87,6 +88,7 @@
     srcs: [":libinputflinger_sources"],
     shared_libs: [
         "android.hardware.input.processor-V1-ndk",
+        "com.android.server.inputflinger-ndk",
         "libbase",
         "libbinder",
         "libbinder_ndk",
@@ -107,6 +109,14 @@
         "libpalmrejection",
         "libui-types",
     ],
+    generated_headers: [
+        "cxx-bridge-header",
+        "inputflinger_rs_bootstrap_bridge_header",
+    ],
+    header_libs: ["inputflinger_rs_bootstrap_cxx_headers"],
+    generated_sources: ["inputflinger_rs_bootstrap_bridge_code"],
+    whole_static_libs: ["libinputflinger_rs"],
+    export_shared_lib_headers: ["com.android.server.inputflinger-ndk"],
     target: {
         android: {
             shared_libs: [
diff --git a/services/inputflinger/InputListener.cpp b/services/inputflinger/InputListener.cpp
index aa55873..d319d6d 100644
--- a/services/inputflinger/InputListener.cpp
+++ b/services/inputflinger/InputListener.cpp
@@ -24,7 +24,7 @@
 
 #include <android-base/stringprintf.h>
 #include <android/log.h>
-#include <utils/Trace.h>
+#include <input/TraceTools.h>
 
 using android::base::StringPrintf;
 
@@ -61,58 +61,42 @@
 
 // --- QueuedInputListener ---
 
-static inline void traceEvent(const char* functionName, int32_t id) {
-    if (ATRACE_ENABLED()) {
-        std::string message = StringPrintf("%s(id=0x%" PRIx32 ")", functionName, id);
-        ATRACE_NAME(message.c_str());
-    }
-}
-
 QueuedInputListener::QueuedInputListener(InputListenerInterface& innerListener)
       : mInnerListener(innerListener) {}
 
 void QueuedInputListener::notifyInputDevicesChanged(const NotifyInputDevicesChangedArgs& args) {
-    traceEvent(__func__, args.id);
     mArgsQueue.emplace_back(args);
 }
 
 void QueuedInputListener::notifyConfigurationChanged(const NotifyConfigurationChangedArgs& args) {
-    traceEvent(__func__, args.id);
     mArgsQueue.emplace_back(args);
 }
 
 void QueuedInputListener::notifyKey(const NotifyKeyArgs& args) {
-    traceEvent(__func__, args.id);
     mArgsQueue.emplace_back(args);
 }
 
 void QueuedInputListener::notifyMotion(const NotifyMotionArgs& args) {
-    traceEvent(__func__, args.id);
     mArgsQueue.emplace_back(args);
 }
 
 void QueuedInputListener::notifySwitch(const NotifySwitchArgs& args) {
-    traceEvent(__func__, args.id);
     mArgsQueue.emplace_back(args);
 }
 
 void QueuedInputListener::notifySensor(const NotifySensorArgs& args) {
-    traceEvent(__func__, args.id);
     mArgsQueue.emplace_back(args);
 }
 
 void QueuedInputListener::notifyVibratorState(const NotifyVibratorStateArgs& args) {
-    traceEvent(__func__, args.id);
     mArgsQueue.emplace_back(args);
 }
 
 void QueuedInputListener::notifyDeviceReset(const NotifyDeviceResetArgs& args) {
-    traceEvent(__func__, args.id);
     mArgsQueue.emplace_back(args);
 }
 
 void QueuedInputListener::notifyPointerCaptureChanged(const NotifyPointerCaptureChangedArgs& args) {
-    traceEvent(__func__, args.id);
     mArgsQueue.emplace_back(args);
 }
 
@@ -123,4 +107,81 @@
     mArgsQueue.clear();
 }
 
+// --- TracedInputListener ---
+
+TracedInputListener::TracedInputListener(const char* name, InputListenerInterface& innerListener)
+      : mInnerListener(innerListener), mName(name) {}
+
+void TracedInputListener::notifyInputDevicesChanged(const NotifyInputDevicesChangedArgs& args) {
+    constexpr static auto& fnName = __func__;
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("%s::%s(id=0x%" PRIx32 ")", mName, fnName, args.id);
+    });
+    mInnerListener.notify(args);
+}
+
+void TracedInputListener::notifyConfigurationChanged(const NotifyConfigurationChangedArgs& args) {
+    constexpr static auto& fnName = __func__;
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("%s::%s(id=0x%" PRIx32 ")", mName, fnName, args.id);
+    });
+    mInnerListener.notify(args);
+}
+
+void TracedInputListener::notifyKey(const NotifyKeyArgs& args) {
+    constexpr static auto& fnName = __func__;
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("%s::%s(id=0x%" PRIx32 ")", mName, fnName, args.id);
+    });
+    mInnerListener.notify(args);
+}
+
+void TracedInputListener::notifyMotion(const NotifyMotionArgs& args) {
+    constexpr static auto& fnName = __func__;
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("%s::%s(id=0x%" PRIx32 ")", mName, fnName, args.id);
+    });
+    mInnerListener.notify(args);
+}
+
+void TracedInputListener::notifySwitch(const NotifySwitchArgs& args) {
+    constexpr static auto& fnName = __func__;
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("%s::%s(id=0x%" PRIx32 ")", mName, fnName, args.id);
+    });
+    mInnerListener.notify(args);
+}
+
+void TracedInputListener::notifySensor(const NotifySensorArgs& args) {
+    constexpr static auto& fnName = __func__;
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("%s::%s(id=0x%" PRIx32 ")", mName, fnName, args.id);
+    });
+    mInnerListener.notify(args);
+}
+
+void TracedInputListener::notifyVibratorState(const NotifyVibratorStateArgs& args) {
+    constexpr static auto& fnName = __func__;
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("%s::%s(id=0x%" PRIx32 ")", mName, fnName, args.id);
+    });
+    mInnerListener.notify(args);
+}
+
+void TracedInputListener::notifyDeviceReset(const NotifyDeviceResetArgs& args) {
+    constexpr static auto& fnName = __func__;
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("%s::%s(id=0x%" PRIx32 ")", mName, fnName, args.id);
+    });
+    mInnerListener.notify(args);
+}
+
+void TracedInputListener::notifyPointerCaptureChanged(const NotifyPointerCaptureChangedArgs& args) {
+    constexpr static auto& fnName = __func__;
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("%s::%s(id=0x%" PRIx32 ")", mName, fnName, args.id);
+    });
+    mInnerListener.notify(args);
+}
+
 } // namespace android
diff --git a/services/inputflinger/InputManager.cpp b/services/inputflinger/InputManager.cpp
index c903564..0567a32 100644
--- a/services/inputflinger/InputManager.cpp
+++ b/services/inputflinger/InputManager.cpp
@@ -23,22 +23,25 @@
 #include "InputReaderFactory.h"
 #include "UnwantedInteractionBlocker.h"
 
+#include <aidl/com/android/server/inputflinger/IInputFlingerRust.h>
+#include <android/binder_interface_utils.h>
 #include <android/sysprop/InputProperties.sysprop.h>
 #include <binder/IPCThreadState.h>
-
+#include <inputflinger_bootstrap.rs.h>
 #include <log/log.h>
-#include <unordered_map>
-
 #include <private/android_filesystem_config.h>
 
 namespace android {
 
-static const bool ENABLE_INPUT_DEVICE_USAGE_METRICS =
+namespace {
+
+const bool ENABLE_INPUT_DEVICE_USAGE_METRICS =
         sysprop::InputProperties::enable_input_device_usage_metrics().value_or(true);
 
-using gui::FocusRequest;
+const bool ENABLE_POINTER_CHOREOGRAPHER =
+        sysprop::InputProperties::enable_pointer_choreographer().value_or(false);
 
-static int32_t exceptionCodeFromStatusT(status_t status) {
+int32_t exceptionCodeFromStatusT(status_t status) {
     switch (status) {
         case OK:
             return binder::Status::EX_NONE;
@@ -57,26 +60,98 @@
     }
 }
 
+// Convert a binder interface into a raw pointer to an AIBinder.
+using IInputFlingerRustBootstrapCallback = aidl::com::android::server::inputflinger::
+        IInputFlingerRust::IInputFlingerRustBootstrapCallback;
+IInputFlingerRustBootstrapCallbackAIBinder* binderToPointer(
+        IInputFlingerRustBootstrapCallback& interface) {
+    ndk::SpAIBinder spAIBinder = interface.asBinder();
+    auto* ptr = spAIBinder.get();
+    AIBinder_incStrong(ptr);
+    return ptr;
+}
+
+// Create the Rust component of InputFlinger that uses AIDL interfaces as a the foreign function
+// interface (FFI). The bootstraping process for IInputFlingerRust is as follows:
+//   - Create BnInputFlingerRustBootstrapCallback in C++.
+//   - Use the cxxbridge ffi interface to call the Rust function `create_inputflinger_rust()`, and
+//     pass the callback binder object as a raw pointer.
+//   - The Rust implementation will create the implementation of IInputFlingerRust, and pass it
+//     to C++ through the callback.
+//   - After the Rust function returns, the binder interface provided to the callback will be the
+//     only strong reference to the IInputFlingerRust.
+std::shared_ptr<IInputFlingerRust> createInputFlingerRust() {
+    using namespace aidl::com::android::server::inputflinger;
+
+    class Callback : public IInputFlingerRust::BnInputFlingerRustBootstrapCallback {
+        ndk::ScopedAStatus onProvideInputFlingerRust(
+                const std::shared_ptr<IInputFlingerRust>& inputFlingerRust) override {
+            mService = inputFlingerRust;
+            return ndk::ScopedAStatus::ok();
+        }
+
+    public:
+        std::shared_ptr<IInputFlingerRust> consumeInputFlingerRust() {
+            auto service = mService;
+            mService.reset();
+            return service;
+        }
+
+    private:
+        std::shared_ptr<IInputFlingerRust> mService;
+    };
+
+    auto callback = ndk::SharedRefBase::make<Callback>();
+    create_inputflinger_rust(binderToPointer(*callback));
+    auto service = callback->consumeInputFlingerRust();
+    LOG_ALWAYS_FATAL_IF(!service,
+                        "create_inputflinger_rust did not provide the IInputFlingerRust "
+                        "implementation through the callback.");
+    return service;
+}
+
+} // namespace
+
 /**
  * The event flow is via the "InputListener" interface, as follows:
  *   InputReader
  *     -> UnwantedInteractionBlocker
+ *     -> PointerChoreographer
  *     -> InputProcessor
  *     -> InputDeviceMetricsCollector
  *     -> InputDispatcher
  */
 InputManager::InputManager(const sp<InputReaderPolicyInterface>& readerPolicy,
-                           InputDispatcherPolicyInterface& dispatcherPolicy) {
+                           InputDispatcherPolicyInterface& dispatcherPolicy,
+                           PointerChoreographerPolicyInterface& choreographerPolicy) {
+    mInputFlingerRust = createInputFlingerRust();
+
     mDispatcher = createInputDispatcher(dispatcherPolicy);
+    mTracingStages.emplace_back(
+            std::make_unique<TracedInputListener>("InputDispatcher", *mDispatcher));
 
     if (ENABLE_INPUT_DEVICE_USAGE_METRICS) {
-        mCollector = std::make_unique<InputDeviceMetricsCollector>(*mDispatcher);
+        mCollector = std::make_unique<InputDeviceMetricsCollector>(*mTracingStages.back());
+        mTracingStages.emplace_back(
+                std::make_unique<TracedInputListener>("MetricsCollector", *mCollector));
     }
 
-    mProcessor = ENABLE_INPUT_DEVICE_USAGE_METRICS ? std::make_unique<InputProcessor>(*mCollector)
-                                                   : std::make_unique<InputProcessor>(*mDispatcher);
-    mBlocker = std::make_unique<UnwantedInteractionBlocker>(*mProcessor);
-    mReader = createInputReader(readerPolicy, *mBlocker);
+    mProcessor = std::make_unique<InputProcessor>(*mTracingStages.back());
+    mTracingStages.emplace_back(
+            std::make_unique<TracedInputListener>("InputProcessor", *mProcessor));
+
+    if (ENABLE_POINTER_CHOREOGRAPHER) {
+        mChoreographer =
+                std::make_unique<PointerChoreographer>(*mTracingStages.back(), choreographerPolicy);
+        mTracingStages.emplace_back(
+                std::make_unique<TracedInputListener>("PointerChoreographer", *mChoreographer));
+    }
+
+    mBlocker = std::make_unique<UnwantedInteractionBlocker>(*mTracingStages.back());
+    mTracingStages.emplace_back(
+            std::make_unique<TracedInputListener>("UnwantedInteractionBlocker", *mBlocker));
+
+    mReader = createInputReader(readerPolicy, *mTracingStages.back());
 }
 
 InputManager::~InputManager() {
@@ -123,6 +198,10 @@
     return *mReader;
 }
 
+PointerChoreographerInterface& InputManager::getChoreographer() {
+    return *mChoreographer;
+}
+
 InputProcessorInterface& InputManager::getProcessor() {
     return *mProcessor;
 }
@@ -147,6 +226,10 @@
     dump += '\n';
     mBlocker->dump(dump);
     dump += '\n';
+    if (ENABLE_POINTER_CHOREOGRAPHER) {
+        mChoreographer->dump(dump);
+        dump += '\n';
+    }
     mProcessor->dump(dump);
     dump += '\n';
     if (ENABLE_INPUT_DEVICE_USAGE_METRICS) {
@@ -190,7 +273,7 @@
     return NO_ERROR;
 }
 
-binder::Status InputManager::setFocusedWindow(const FocusRequest& request) {
+binder::Status InputManager::setFocusedWindow(const gui::FocusRequest& request) {
     mDispatcher->setFocusedWindow(request);
     return binder::Status::ok();
 }
diff --git a/services/inputflinger/InputManager.h b/services/inputflinger/InputManager.h
index 9dc285f..20b9fd5 100644
--- a/services/inputflinger/InputManager.h
+++ b/services/inputflinger/InputManager.h
@@ -23,13 +23,16 @@
 #include "InputDeviceMetricsCollector.h"
 #include "InputProcessor.h"
 #include "InputReaderBase.h"
+#include "PointerChoreographer.h"
 #include "include/UnwantedInteractionBlockerInterface.h"
 
 #include <InputDispatcherInterface.h>
 #include <InputDispatcherPolicyInterface.h>
+#include <PointerChoreographerPolicyInterface.h>
 #include <input/Input.h>
 #include <input/InputTransport.h>
 
+#include <aidl/com/android/server/inputflinger/IInputFlingerRust.h>
 #include <android/os/BnInputFlinger.h>
 #include <utils/Errors.h>
 #include <utils/RefBase.h>
@@ -37,6 +40,8 @@
 
 using android::os::BnInputFlinger;
 
+using aidl::com::android::server::inputflinger::IInputFlingerRust;
+
 namespace android {
 class InputChannel;
 class InputDispatcherThread;
@@ -83,6 +88,9 @@
     /* Gets the input reader. */
     virtual InputReaderInterface& getReader() = 0;
 
+    /* Gets the PointerChoreographer. */
+    virtual PointerChoreographerInterface& getChoreographer() = 0;
+
     /* Gets the input processor. */
     virtual InputProcessorInterface& getProcessor() = 0;
 
@@ -105,12 +113,14 @@
 
 public:
     InputManager(const sp<InputReaderPolicyInterface>& readerPolicy,
-                 InputDispatcherPolicyInterface& dispatcherPolicy);
+                 InputDispatcherPolicyInterface& dispatcherPolicy,
+                 PointerChoreographerPolicyInterface& choreographerPolicy);
 
     status_t start() override;
     status_t stop() override;
 
     InputReaderInterface& getReader() override;
+    PointerChoreographerInterface& getChoreographer() override;
     InputProcessorInterface& getProcessor() override;
     InputDeviceMetricsCollectorInterface& getMetricsCollector() override;
     InputDispatcherInterface& getDispatcher() override;
@@ -127,11 +137,17 @@
 
     std::unique_ptr<UnwantedInteractionBlockerInterface> mBlocker;
 
+    std::unique_ptr<PointerChoreographerInterface> mChoreographer;
+
     std::unique_ptr<InputProcessorInterface> mProcessor;
 
     std::unique_ptr<InputDeviceMetricsCollectorInterface> mCollector;
 
     std::unique_ptr<InputDispatcherInterface> mDispatcher;
+
+    std::shared_ptr<IInputFlingerRust> mInputFlingerRust;
+
+    std::vector<std::unique_ptr<TracedInputListener>> mTracingStages;
 };
 
 } // namespace android
diff --git a/services/inputflinger/PointerChoreographer.cpp b/services/inputflinger/PointerChoreographer.cpp
new file mode 100644
index 0000000..e411abb
--- /dev/null
+++ b/services/inputflinger/PointerChoreographer.cpp
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+#define LOG_TAG "PointerChoreographer"
+
+#include "PointerChoreographer.h"
+
+namespace android {
+
+// --- PointerChoreographer ---
+
+PointerChoreographer::PointerChoreographer(InputListenerInterface& listener,
+                                           PointerChoreographerPolicyInterface& policy)
+      : mNextListener(listener) {}
+
+void PointerChoreographer::notifyInputDevicesChanged(const NotifyInputDevicesChangedArgs& args) {
+    mNextListener.notify(args);
+}
+
+void PointerChoreographer::notifyConfigurationChanged(const NotifyConfigurationChangedArgs& args) {
+    mNextListener.notify(args);
+}
+
+void PointerChoreographer::notifyKey(const NotifyKeyArgs& args) {
+    mNextListener.notify(args);
+}
+
+void PointerChoreographer::notifyMotion(const NotifyMotionArgs& args) {
+    mNextListener.notify(args);
+}
+
+void PointerChoreographer::notifySwitch(const NotifySwitchArgs& args) {
+    mNextListener.notify(args);
+}
+
+void PointerChoreographer::notifySensor(const NotifySensorArgs& args) {
+    mNextListener.notify(args);
+}
+
+void PointerChoreographer::notifyVibratorState(const NotifyVibratorStateArgs& args) {
+    mNextListener.notify(args);
+}
+
+void PointerChoreographer::notifyDeviceReset(const NotifyDeviceResetArgs& args) {
+    mNextListener.notify(args);
+}
+
+void PointerChoreographer::notifyPointerCaptureChanged(
+        const NotifyPointerCaptureChangedArgs& args) {
+    mNextListener.notify(args);
+}
+
+void PointerChoreographer::dump(std::string& dump) {
+    dump += "PointerChoreographer:\n";
+}
+
+} // namespace android
diff --git a/services/inputflinger/PointerChoreographer.h b/services/inputflinger/PointerChoreographer.h
new file mode 100644
index 0000000..5e5f782
--- /dev/null
+++ b/services/inputflinger/PointerChoreographer.h
@@ -0,0 +1,61 @@
+/*
+ * 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 "InputListener.h"
+#include "NotifyArgs.h"
+#include "PointerChoreographerPolicyInterface.h"
+
+namespace android {
+
+/**
+ * PointerChoreographer manages the icons shown by the system for input interactions.
+ * This includes showing the mouse cursor, stylus hover icons, and touch spots.
+ * It is responsible for accumulating the location of the mouse cursor, and populating
+ * the cursor position for incoming events, if necessary.
+ */
+class PointerChoreographerInterface : public InputListenerInterface {
+public:
+    /**
+     * This method may be called on any thread (usually by the input manager on a binder thread).
+     */
+    virtual void dump(std::string& dump) = 0;
+};
+
+class PointerChoreographer : public PointerChoreographerInterface {
+public:
+    explicit PointerChoreographer(InputListenerInterface& listener,
+                                  PointerChoreographerPolicyInterface&);
+    ~PointerChoreographer() override = default;
+
+    void notifyInputDevicesChanged(const NotifyInputDevicesChangedArgs& args) override;
+    void notifyConfigurationChanged(const NotifyConfigurationChangedArgs& args) override;
+    void notifyKey(const NotifyKeyArgs& args) override;
+    void notifyMotion(const NotifyMotionArgs& args) override;
+    void notifySwitch(const NotifySwitchArgs& args) override;
+    void notifySensor(const NotifySensorArgs& args) override;
+    void notifyVibratorState(const NotifyVibratorStateArgs& args) override;
+    void notifyDeviceReset(const NotifyDeviceResetArgs& args) override;
+    void notifyPointerCaptureChanged(const NotifyPointerCaptureChangedArgs& args) override;
+
+    void dump(std::string& dump) override;
+
+private:
+    InputListenerInterface& mNextListener;
+};
+
+} // namespace android
diff --git a/services/inputflinger/TEST_MAPPING b/services/inputflinger/TEST_MAPPING
index c2da5ba..ee1f3cb 100644
--- a/services/inputflinger/TEST_MAPPING
+++ b/services/inputflinger/TEST_MAPPING
@@ -72,9 +72,6 @@
           "include-filter": "android.view.cts.TouchDelegateTest"
         },
         {
-          "include-filter": "android.view.cts.VelocityTrackerTest"
-        },
-        {
           "include-filter": "android.view.cts.VerifyInputEventTest"
         },
         {
@@ -218,9 +215,6 @@
           "include-filter": "android.view.cts.TouchDelegateTest"
         },
         {
-          "include-filter": "android.view.cts.VelocityTrackerTest"
-        },
-        {
           "include-filter": "android.view.cts.VerifyInputEventTest"
         },
         {
diff --git a/services/inputflinger/aidl/Android.bp b/services/inputflinger/aidl/Android.bp
new file mode 100644
index 0000000..314c433
--- /dev/null
+++ b/services/inputflinger/aidl/Android.bp
@@ -0,0 +1,31 @@
+// 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.
+
+aidl_interface {
+    name: "com.android.server.inputflinger",
+    srcs: ["**/*.aidl"],
+    unstable: true,
+    host_supported: true,
+    backend: {
+        cpp: {
+            enabled: false,
+        },
+        java: {
+            enabled: false,
+        },
+        rust: {
+            enabled: true,
+        },
+    },
+}
diff --git a/services/inputflinger/aidl/com/android/server/inputflinger/IInputFlingerRust.aidl b/services/inputflinger/aidl/com/android/server/inputflinger/IInputFlingerRust.aidl
new file mode 100644
index 0000000..8e826fd
--- /dev/null
+++ b/services/inputflinger/aidl/com/android/server/inputflinger/IInputFlingerRust.aidl
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.inputflinger;
+
+/**
+ * A local AIDL interface used as a foreign function interface (ffi) to
+ * communicate with the Rust component of inputflinger.
+ *
+ * NOTE: Since we use this as a local interface, all processing happens on the
+ * calling thread.
+ */
+interface IInputFlingerRust {
+
+   /**
+    * An interface used to get a strong reference to IInputFlingerRust on boot.
+    */
+    interface IInputFlingerRustBootstrapCallback {
+        void onProvideInputFlingerRust(in IInputFlingerRust inputFlingerRust);
+    }
+}
diff --git a/services/inputflinger/benchmarks/InputDispatcher_benchmarks.cpp b/services/inputflinger/benchmarks/InputDispatcher_benchmarks.cpp
index 6dd785a..188d5f0 100644
--- a/services/inputflinger/benchmarks/InputDispatcher_benchmarks.cpp
+++ b/services/inputflinger/benchmarks/InputDispatcher_benchmarks.cpp
@@ -185,10 +185,7 @@
         mInfo.token = mClientChannel->getConnectionToken();
         mInfo.name = "FakeWindowHandle";
         mInfo.dispatchingTimeout = DISPATCHING_TIMEOUT;
-        mInfo.frameLeft = mFrame.left;
-        mInfo.frameTop = mFrame.top;
-        mInfo.frameRight = mFrame.right;
-        mInfo.frameBottom = mFrame.bottom;
+        mInfo.frame = mFrame;
         mInfo.globalScaleFactor = 1.0;
         mInfo.touchableRegion.clear();
         mInfo.addTouchableRegion(mFrame);
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index 2923a3c..640602f 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -32,6 +32,7 @@
 #endif
 #include <input/InputDevice.h>
 #include <input/PrintTools.h>
+#include <input/TraceTools.h>
 #include <openssl/mem.h>
 #include <powermanager/PowerManager.h>
 #include <unistd.h>
@@ -1105,7 +1106,8 @@
         const auto [x, y] = resolveTouchedPosition(motionEntry);
         const bool isStylus = isPointerFromStylus(motionEntry, /*pointerIndex=*/0);
 
-        auto [touchedWindowHandle, _] = findTouchedWindowAtLocked(displayId, x, y, isStylus);
+        sp<WindowInfoHandle> touchedWindowHandle =
+                findTouchedWindowAtLocked(displayId, x, y, isStylus);
         if (touchedWindowHandle != nullptr &&
             touchedWindowHandle->getApplicationToken() !=
                     mAwaitedFocusedApplication->getApplicationToken()) {
@@ -1229,11 +1231,10 @@
     }
 }
 
-std::pair<sp<WindowInfoHandle>, std::vector<InputTarget>>
-InputDispatcher::findTouchedWindowAtLocked(int32_t displayId, float x, float y, bool isStylus,
-                                           bool ignoreDragWindow) const {
+sp<WindowInfoHandle> InputDispatcher::findTouchedWindowAtLocked(int32_t displayId, float x, float y,
+                                                                bool isStylus,
+                                                                bool ignoreDragWindow) const {
     // Traverse windows from front to back to find touched window.
-    std::vector<InputTarget> outsideTargets;
     const auto& windowHandles = getWindowHandlesLocked(displayId);
     for (const sp<WindowInfoHandle>& windowHandle : windowHandles) {
         if (ignoreDragWindow && haveSameToken(windowHandle, mDragState->dragWindow)) {
@@ -1243,16 +1244,35 @@
         const WindowInfo& info = *windowHandle->getInfo();
         if (!info.isSpy() &&
             windowAcceptsTouchAt(info, displayId, x, y, isStylus, getTransformLocked(displayId))) {
-            return {windowHandle, outsideTargets};
+            return windowHandle;
+        }
+    }
+    return nullptr;
+}
+
+std::vector<InputTarget> InputDispatcher::findOutsideTargetsLocked(
+        int32_t displayId, const sp<WindowInfoHandle>& touchedWindow) const {
+    if (touchedWindow == nullptr) {
+        return {};
+    }
+    // Traverse windows from front to back until we encounter the touched window.
+    std::vector<InputTarget> outsideTargets;
+    const auto& windowHandles = getWindowHandlesLocked(displayId);
+    for (const sp<WindowInfoHandle>& windowHandle : windowHandles) {
+        if (windowHandle == touchedWindow) {
+            // Stop iterating once we found a touched window. Any WATCH_OUTSIDE_TOUCH window
+            // below the touched window will not get ACTION_OUTSIDE event.
+            return outsideTargets;
         }
 
+        const WindowInfo& info = *windowHandle->getInfo();
         if (info.inputConfig.test(WindowInfo::InputConfig::WATCH_OUTSIDE_TOUCH)) {
             addWindowTargetLocked(windowHandle, InputTarget::Flags::DISPATCH_AS_OUTSIDE,
                                   /*pointerIds=*/{}, /*firstDownTimeInTarget=*/std::nullopt,
                                   outsideTargets);
         }
     }
-    return {nullptr, {}};
+    return outsideTargets;
 }
 
 std::vector<sp<WindowInfoHandle>> InputDispatcher::findTouchedSpyWindowsAtLocked(
@@ -2308,11 +2328,11 @@
         // Outside targets should be added upon first dispatched DOWN event. That means, this should
         // be a pointer that would generate ACTION_DOWN, *and* touch should not already be down.
         const bool isStylus = isPointerFromStylus(entry, pointerIndex);
-        auto [newTouchedWindowHandle, outsideTargets] =
+        sp<WindowInfoHandle> newTouchedWindowHandle =
                 findTouchedWindowAtLocked(displayId, x, y, isStylus);
 
         if (isDown) {
-            targets += outsideTargets;
+            targets += findOutsideTargetsLocked(displayId, newTouchedWindowHandle);
         }
         // Handle the case where we did not find a window.
         if (newTouchedWindowHandle == nullptr) {
@@ -2488,7 +2508,8 @@
             sp<WindowInfoHandle> oldTouchedWindowHandle =
                     tempTouchState.getFirstForegroundWindowHandle();
             LOG_ALWAYS_FATAL_IF(oldTouchedWindowHandle == nullptr);
-            auto [newTouchedWindowHandle, _] = findTouchedWindowAtLocked(displayId, x, y, isStylus);
+            sp<WindowInfoHandle> newTouchedWindowHandle =
+                    findTouchedWindowAtLocked(displayId, x, y, isStylus);
 
             // Verify targeted injection.
             if (const auto err = verifyTargetedInjection(newTouchedWindowHandle, entry); err) {
@@ -2734,7 +2755,7 @@
     // have an explicit reason to support it.
     constexpr bool isStylus = false;
 
-    auto [dropWindow, _] =
+    sp<WindowInfoHandle> dropWindow =
             findTouchedWindowAtLocked(displayId, x, y, isStylus, /*ignoreDragWindow=*/true);
     if (dropWindow) {
         vec2 local = dropWindow->getInfo()->transform.transform(x, y);
@@ -2788,8 +2809,9 @@
             // until we have an explicit reason to support it.
             constexpr bool isStylus = false;
 
-            auto [hoverWindowHandle, _] = findTouchedWindowAtLocked(entry.displayId, x, y, isStylus,
-                                                                    /*ignoreDragWindow=*/true);
+            sp<WindowInfoHandle> hoverWindowHandle =
+                    findTouchedWindowAtLocked(entry.displayId, x, y, isStylus,
+                                              /*ignoreDragWindow=*/true);
             // enqueue drag exit if needed.
             if (hoverWindowHandle != mDragState->dragHoverWindowHandle &&
                 !haveSameToken(hoverWindowHandle, mDragState->dragHoverWindowHandle)) {
@@ -3011,8 +3033,8 @@
                                 "hasToken=%s, applicationInfo.name=%s, applicationInfo.token=%s\n",
                         isTouchedWindow ? "[TOUCHED] " : "", info->packageName.c_str(),
                         info->ownerUid.toString().c_str(), info->id,
-                        toString(info->touchOcclusionMode).c_str(), info->alpha, info->frameLeft,
-                        info->frameTop, info->frameRight, info->frameBottom,
+                        toString(info->touchOcclusionMode).c_str(), info->alpha, info->frame.left,
+                        info->frame.top, info->frame.right, info->frame.bottom,
                         dumpRegion(info->touchableRegion).c_str(), info->name.c_str(),
                         info->inputConfig.string().c_str(), toString(info->token != nullptr),
                         info->applicationInfo.name.c_str(),
@@ -3161,12 +3183,10 @@
                                                  const std::shared_ptr<Connection>& connection,
                                                  std::shared_ptr<EventEntry> eventEntry,
                                                  const InputTarget& inputTarget) {
-    if (ATRACE_ENABLED()) {
-        std::string message =
-                StringPrintf("prepareDispatchCycleLocked(inputChannel=%s, id=0x%" PRIx32 ")",
-                             connection->getInputChannelName().c_str(), eventEntry->id);
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("prepareDispatchCycleLocked(inputChannel=%s, id=0x%" PRIx32 ")",
+                            connection->getInputChannelName().c_str(), eventEntry->id);
+    });
     if (DEBUG_DISPATCH_CYCLE) {
         ALOGD("channel '%s' ~ prepareDispatchCycle - flags=%s, "
               "globalScaleFactor=%f, pointerIds=%s %s",
@@ -3231,12 +3251,10 @@
                                                    const std::shared_ptr<Connection>& connection,
                                                    std::shared_ptr<EventEntry> eventEntry,
                                                    const InputTarget& inputTarget) {
-    if (ATRACE_ENABLED()) {
-        std::string message =
-                StringPrintf("enqueueDispatchEntriesLocked(inputChannel=%s, id=0x%" PRIx32 ")",
-                             connection->getInputChannelName().c_str(), eventEntry->id);
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("enqueueDispatchEntriesLocked(inputChannel=%s, id=0x%" PRIx32 ")",
+                            connection->getInputChannelName().c_str(), eventEntry->id);
+    });
     LOG_ALWAYS_FATAL_IF(!inputTarget.flags.any(InputTarget::DISPATCH_MASK),
                         "No dispatch flags are set for %s", eventEntry->getDescription().c_str());
 
@@ -3266,12 +3284,6 @@
                                                  std::shared_ptr<EventEntry> eventEntry,
                                                  const InputTarget& inputTarget,
                                                  ftl::Flags<InputTarget::Flags> dispatchMode) {
-    if (ATRACE_ENABLED()) {
-        std::string message = StringPrintf("enqueueDispatchEntry(inputChannel=%s, dispatchMode=%s)",
-                                           connection->getInputChannelName().c_str(),
-                                           dispatchMode.string().c_str());
-        ATRACE_NAME(message.c_str());
-    }
     ftl::Flags<InputTarget::Flags> inputTargetFlags = inputTarget.flags;
     if (!inputTargetFlags.any(dispatchMode)) {
         return;
@@ -3558,11 +3570,10 @@
 
 void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
                                                const std::shared_ptr<Connection>& connection) {
-    if (ATRACE_ENABLED()) {
-        std::string message = StringPrintf("startDispatchCycleLocked(inputChannel=%s)",
-                                           connection->getInputChannelName().c_str());
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("startDispatchCycleLocked(inputChannel=%s)",
+                            connection->getInputChannelName().c_str());
+    });
     if (DEBUG_DISPATCH_CYCLE) {
         ALOGD("channel '%s' ~ startDispatchCycle", connection->getInputChannelName().c_str());
     }
@@ -4136,12 +4147,10 @@
     }
 
     int32_t newId = mIdGenerator.nextId();
-    if (ATRACE_ENABLED()) {
-        std::string message = StringPrintf("Split MotionEvent(id=0x%" PRIx32
-                                           ") to MotionEvent(id=0x%" PRIx32 ").",
-                                           originalMotionEntry.id, newId);
-        ATRACE_NAME(message.c_str());
-    }
+    ATRACE_NAME_IF(ATRACE_ENABLED(), [&]() {
+        return StringPrintf("Split MotionEvent(id=0x%" PRIx32 ") to MotionEvent(id=0x%" PRIx32 ").",
+                            originalMotionEntry.id, newId);
+    });
     std::unique_ptr<MotionEntry> splitMotionEntry =
             std::make_unique<MotionEntry>(newId, originalMotionEntry.eventTime,
                                           originalMotionEntry.deviceId, originalMotionEntry.source,
@@ -5644,9 +5653,9 @@
                                          i, windowInfo->name.c_str(), windowInfo->id,
                                          windowInfo->displayId,
                                          windowInfo->inputConfig.string().c_str(),
-                                         windowInfo->alpha, windowInfo->frameLeft,
-                                         windowInfo->frameTop, windowInfo->frameRight,
-                                         windowInfo->frameBottom, windowInfo->globalScaleFactor,
+                                         windowInfo->alpha, windowInfo->frame.left,
+                                         windowInfo->frame.top, windowInfo->frame.right,
+                                         windowInfo->frame.bottom, windowInfo->globalScaleFactor,
                                          windowInfo->applicationInfo.name.c_str(),
                                          binderToString(windowInfo->applicationInfo.token).c_str());
                     dump += dumpRegion(windowInfo->touchableRegion);
diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h
index fef726f..58ae9c7 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.h
+++ b/services/inputflinger/dispatcher/InputDispatcher.h
@@ -238,9 +238,12 @@
     // to transfer focus to a new application.
     std::shared_ptr<EventEntry> mNextUnblockedEvent GUARDED_BY(mLock);
 
-    std::pair<sp<android::gui::WindowInfoHandle>, std::vector<InputTarget>>
-    findTouchedWindowAtLocked(int32_t displayId, float x, float y, bool isStylus = false,
-                              bool ignoreDragWindow = false) const REQUIRES(mLock);
+    sp<android::gui::WindowInfoHandle> findTouchedWindowAtLocked(
+            int32_t displayId, float x, float y, bool isStylus = false,
+            bool ignoreDragWindow = false) const REQUIRES(mLock);
+    std::vector<InputTarget> findOutsideTargetsLocked(
+            int32_t displayId, const sp<android::gui::WindowInfoHandle>& touchedWindow) const
+            REQUIRES(mLock);
 
     std::vector<sp<android::gui::WindowInfoHandle>> findTouchedSpyWindowsAtLocked(
             int32_t displayId, float x, float y, bool isStylus) const REQUIRES(mLock);
diff --git a/services/inputflinger/host/InputDriver.cpp b/services/inputflinger/host/InputDriver.cpp
index ec0388d..de99fc7 100644
--- a/services/inputflinger/host/InputDriver.cpp
+++ b/services/inputflinger/host/InputDriver.cpp
@@ -256,14 +256,14 @@
 
 const char* InputDriver::inputGetPropertyKey(input_property_t* property) {
     if (property != nullptr) {
-        return property->key.string();
+        return property->key.c_str();
     }
     return nullptr;
 }
 
 const char* InputDriver::inputGetPropertyValue(input_property_t* property) {
     if (property != nullptr) {
-        return property->value.string();
+        return property->value.c_str();
     }
     return nullptr;
 }
@@ -281,7 +281,7 @@
 }
 
 void InputDriver::dump(String8& result) {
-    result.appendFormat(INDENT2 "HAL Input Driver (%s)\n", mName.string());
+    result.appendFormat(INDENT2 "HAL Input Driver (%s)\n", mName.c_str());
 }
 
 } // namespace android
diff --git a/services/inputflinger/host/InputFlinger.cpp b/services/inputflinger/host/InputFlinger.cpp
index 2da2a70..d974c43 100644
--- a/services/inputflinger/host/InputFlinger.cpp
+++ b/services/inputflinger/host/InputFlinger.cpp
@@ -57,7 +57,7 @@
     } else {
         dumpInternal(result);
     }
-    write(fd, result.string(), result.size());
+    write(fd, result.c_str(), result.size());
     return OK;
 }
 
diff --git a/services/inputflinger/include/InputListener.h b/services/inputflinger/include/InputListener.h
index 4f78f03..0b7f7c2 100644
--- a/services/inputflinger/include/InputListener.h
+++ b/services/inputflinger/include/InputListener.h
@@ -76,4 +76,26 @@
     std::vector<NotifyArgs> mArgsQueue;
 };
 
+/*
+ * An implementation of the listener interface that traces the calls to its inner listener.
+ */
+class TracedInputListener : public InputListenerInterface {
+public:
+    explicit TracedInputListener(const char* name, InputListenerInterface& innerListener);
+
+    virtual void notifyInputDevicesChanged(const NotifyInputDevicesChangedArgs& args) override;
+    virtual void notifyConfigurationChanged(const NotifyConfigurationChangedArgs& args) override;
+    virtual void notifyKey(const NotifyKeyArgs& args) override;
+    virtual void notifyMotion(const NotifyMotionArgs& args) override;
+    virtual void notifySwitch(const NotifySwitchArgs& args) override;
+    virtual void notifySensor(const NotifySensorArgs& args) override;
+    virtual void notifyDeviceReset(const NotifyDeviceResetArgs& args) override;
+    void notifyVibratorState(const NotifyVibratorStateArgs& args) override;
+    void notifyPointerCaptureChanged(const NotifyPointerCaptureChangedArgs& args) override;
+
+private:
+    InputListenerInterface& mInnerListener;
+    const char* mName;
+};
+
 } // namespace android
diff --git a/services/inputflinger/include/PointerChoreographerPolicyInterface.h b/services/inputflinger/include/PointerChoreographerPolicyInterface.h
new file mode 100644
index 0000000..9e020c7
--- /dev/null
+++ b/services/inputflinger/include/PointerChoreographerPolicyInterface.h
@@ -0,0 +1,44 @@
+/*
+ * 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 "PointerControllerInterface.h"
+
+namespace android {
+
+/**
+ * The PointerChoreographer policy interface.
+ *
+ * This is the interface that PointerChoreographer uses to talk to Window Manager and other
+ * system components.
+ */
+class PointerChoreographerPolicyInterface {
+public:
+    virtual ~PointerChoreographerPolicyInterface() = default;
+
+    /**
+     * A factory method for PointerController. The PointerController implementation has
+     * dependencies on a graphical library - libgui, used to draw icons on the screen - which
+     * isn't available for the host. Since we want libinputflinger and its test to be buildable
+     * for and runnable on the host, the PointerController implementation must be in a separate
+     * library, libinputservice, that has the additional dependencies. The PointerController
+     * will be mocked when testing PointerChoreographer.
+     */
+    virtual std::shared_ptr<PointerControllerInterface> createPointerController() = 0;
+};
+
+} // namespace android
diff --git a/services/inputflinger/reader/Android.bp b/services/inputflinger/reader/Android.bp
index c0c6d1f..8fe6411 100644
--- a/services/inputflinger/reader/Android.bp
+++ b/services/inputflinger/reader/Android.bp
@@ -66,6 +66,7 @@
         "mapper/accumulator/TouchButtonAccumulator.cpp",
         "mapper/gestures/GestureConverter.cpp",
         "mapper/gestures/GesturesLogging.cpp",
+        "mapper/gestures/HardwareProperties.cpp",
         "mapper/gestures/HardwareStateConverter.cpp",
         "mapper/gestures/PropertyProvider.cpp",
     ],
diff --git a/services/inputflinger/reader/EventHub.cpp b/services/inputflinger/reader/EventHub.cpp
index e69c99e..f7bbc51 100644
--- a/services/inputflinger/reader/EventHub.cpp
+++ b/services/inputflinger/reader/EventHub.cpp
@@ -2473,6 +2473,7 @@
 
         // See if this device has any stylus buttons that we would want to fuse with touch data.
         if (!device->classes.any(InputDeviceClass::TOUCH | InputDeviceClass::TOUCH_MT) &&
+            !device->classes.any(InputDeviceClass::ALPHAKEY) &&
             std::any_of(STYLUS_BUTTON_KEYCODES.begin(), STYLUS_BUTTON_KEYCODES.end(),
                         [&](int32_t keycode) { return device->hasKeycodeLocked(keycode); })) {
             device->classes |= InputDeviceClass::EXTERNAL_STYLUS;
diff --git a/services/inputflinger/reader/InputDevice.cpp b/services/inputflinger/reader/InputDevice.cpp
index bacc720..fb32f96 100644
--- a/services/inputflinger/reader/InputDevice.cpp
+++ b/services/inputflinger/reader/InputDevice.cpp
@@ -66,23 +66,38 @@
     return enabled;
 }
 
-std::list<NotifyArgs> InputDevice::setEnabled(bool enabled, nsecs_t when) {
-    std::list<NotifyArgs> out;
-    if (enabled && mAssociatedDisplayPort && !mAssociatedViewport) {
-        ALOGW("Cannot enable input device %s because it is associated with port %" PRIu8 ", "
-              "but the corresponding viewport is not found",
-              getName().c_str(), *mAssociatedDisplayPort);
-        enabled = false;
+std::list<NotifyArgs> InputDevice::updateEnableState(nsecs_t when,
+                                                     const InputReaderConfiguration& readerConfig,
+                                                     bool forceEnable) {
+    bool enable = forceEnable;
+    if (!forceEnable) {
+        // If the device was explicitly disabled by the user, it would be present in the
+        // "disabledDevices" list. This device should be disabled.
+        enable = readerConfig.disabledDevices.find(mId) == readerConfig.disabledDevices.end();
+
+        // If a device is associated with a specific display but there is no
+        // associated DisplayViewport, don't enable the device.
+        if (enable && (mAssociatedDisplayPort || mAssociatedDisplayUniqueId) &&
+            !mAssociatedViewport) {
+            const std::string desc = mAssociatedDisplayPort
+                    ? "port " + std::to_string(*mAssociatedDisplayPort)
+                    : "uniqueId " + *mAssociatedDisplayUniqueId;
+            ALOGW("Cannot enable input device %s because it is associated "
+                  "with %s, but the corresponding viewport is not found",
+                  getName().c_str(), desc.c_str());
+            enable = false;
+        }
     }
 
-    if (isEnabled() == enabled) {
+    std::list<NotifyArgs> out;
+    if (isEnabled() == enable) {
         return out;
     }
 
     // When resetting some devices, the driver needs to be queried to ensure that a proper reset is
     // performed. The querying must happen when the device is enabled, so we reset after enabling
     // but before disabling the device. See MultiTouchMotionAccumulator::reset for more information.
-    if (enabled) {
+    if (enable) {
         for_each_subdevice([](auto& context) { context.enableDevice(); });
         out += reset(when);
     } else {
@@ -158,18 +173,25 @@
     mDevices.insert({eventHubId, std::make_pair(std::move(contextPtr), std::move(mappers))});
 }
 
-void InputDevice::addEventHubDevice(int32_t eventHubId,
-                                    const InputReaderConfiguration& readerConfig) {
+[[nodiscard]] std::list<NotifyArgs> InputDevice::addEventHubDevice(
+        nsecs_t when, int32_t eventHubId, const InputReaderConfiguration& readerConfig) {
     if (mDevices.find(eventHubId) != mDevices.end()) {
-        return;
+        return {};
     }
-    std::unique_ptr<InputDeviceContext> contextPtr(new InputDeviceContext(*this, eventHubId));
-    std::vector<std::unique_ptr<InputMapper>> mappers = createMappers(*contextPtr, readerConfig);
 
-    // insert the context into the devices set
-    mDevices.insert({eventHubId, std::make_pair(std::move(contextPtr), std::move(mappers))});
+    // Add an empty device configure and keep it enabled to allow mapper population with correct
+    // configuration/context,
+    // Note: we need to ensure device is kept enabled till mappers are configured
+    // TODO: b/281852638 refactor tests to remove this flag and reliance on the empty device
+    addEmptyEventHubDevice(eventHubId);
+    std::list<NotifyArgs> out = configureInternal(when, readerConfig, {}, /*forceEnable=*/true);
+
+    DevicePair& devicePair = mDevices[eventHubId];
+    devicePair.second = createMappers(*devicePair.first, readerConfig);
+
     // Must change generation to flag this device as changed
     bumpGeneration();
+    return out;
 }
 
 void InputDevice::removeEventHubDevice(int32_t eventHubId) {
@@ -183,6 +205,12 @@
 std::list<NotifyArgs> InputDevice::configure(nsecs_t when,
                                              const InputReaderConfiguration& readerConfig,
                                              ConfigurationChanges changes) {
+    return configureInternal(when, readerConfig, changes);
+}
+std::list<NotifyArgs> InputDevice::configureInternal(nsecs_t when,
+                                                     const InputReaderConfiguration& readerConfig,
+                                                     ConfigurationChanges changes,
+                                                     bool forceEnable) {
     std::list<NotifyArgs> out;
     mSources = 0;
     mClasses = ftl::Flags<InputDeviceClass>(0);
@@ -235,15 +263,6 @@
             }
         }
 
-        if (changes.test(Change::ENABLED_STATE)) {
-            // Do not execute this code on the first configure, because 'setEnabled' would call
-            // InputMapper::reset, and you can't reset a mapper before it has been configured.
-            // The mappers are configured for the first time at the bottom of this function.
-            auto it = readerConfig.disabledDevices.find(mId);
-            bool enabled = it == readerConfig.disabledDevices.end();
-            out += setEnabled(enabled, when);
-        }
-
         if (!changes.any() || changes.test(Change::DISPLAY_INFO)) {
             // In most situations, no port or name will be specified.
             mAssociatedDisplayPort = std::nullopt;
@@ -267,12 +286,8 @@
                 }
             }
 
-            // If the device was explicitly disabled by the user, it would be present in the
-            // "disabledDevices" list. If it is associated with a specific display, and it was not
-            // explicitly disabled, then enable/disable the device based on whether we can find the
-            // corresponding viewport.
-            bool enabled =
-                    (readerConfig.disabledDevices.find(mId) == readerConfig.disabledDevices.end());
+            // If it is associated with a specific display, then find the corresponding viewport
+            // which will be used to enable/disable the device.
             if (mAssociatedDisplayPort) {
                 mAssociatedViewport =
                         readerConfig.getDisplayViewportByPort(*mAssociatedDisplayPort);
@@ -280,7 +295,6 @@
                     ALOGW("Input device %s should be associated with display on port %" PRIu8 ", "
                           "but the corresponding viewport is not found.",
                           getName().c_str(), *mAssociatedDisplayPort);
-                    enabled = false;
                 }
             } else if (mAssociatedDisplayUniqueId != std::nullopt) {
                 mAssociatedViewport =
@@ -289,16 +303,8 @@
                     ALOGW("Input device %s should be associated with display %s but the "
                           "corresponding viewport cannot be found",
                           getName().c_str(), mAssociatedDisplayUniqueId->c_str());
-                    enabled = false;
                 }
             }
-
-            if (changes.any()) {
-                // For first-time configuration, only allow device to be disabled after mappers have
-                // finished configuring. This is because we need to read some of the properties from
-                // the device's open fd.
-                out += setEnabled(enabled, when);
-            }
         }
 
         for_each_mapper([this, when, &readerConfig, changes, &out](InputMapper& mapper) {
@@ -306,12 +312,11 @@
             mSources |= mapper.getSources();
         });
 
-        // If a device is just plugged but it might be disabled, we need to update some info like
-        // axis range of touch from each InputMapper first, then disable it.
-        if (!changes.any()) {
-            out += setEnabled(readerConfig.disabledDevices.find(mId) ==
-                                      readerConfig.disabledDevices.end(),
-                              when);
+        if (!changes.any() || changes.test(Change::ENABLED_STATE) ||
+            changes.test(Change::DISPLAY_INFO)) {
+            // Whether a device is enabled can depend on the display association,
+            // so update the enabled state when there is a change in display info.
+            out += updateEnableState(when, readerConfig, forceEnable);
         }
     }
     return out;
@@ -505,9 +510,9 @@
         classes.test(InputDeviceClass::TOUCH_MT) && !isSonyDualShock4Touchpad) {
         mappers.push_back(createInputMapper<TouchpadInputMapper>(contextPtr, readerConfig));
     } else if (classes.test(InputDeviceClass::TOUCH_MT)) {
-        mappers.push_back(std::make_unique<MultiTouchInputMapper>(contextPtr, readerConfig));
+        mappers.push_back(createInputMapper<MultiTouchInputMapper>(contextPtr, readerConfig));
     } else if (classes.test(InputDeviceClass::TOUCH)) {
-        mappers.push_back(std::make_unique<SingleTouchInputMapper>(contextPtr, readerConfig));
+        mappers.push_back(createInputMapper<SingleTouchInputMapper>(contextPtr, readerConfig));
     }
 
     // Joystick-like devices.
diff --git a/services/inputflinger/reader/InputReader.cpp b/services/inputflinger/reader/InputReader.cpp
index 08600b2..0aea0b3 100644
--- a/services/inputflinger/reader/InputReader.cpp
+++ b/services/inputflinger/reader/InputReader.cpp
@@ -77,7 +77,7 @@
       : mContext(this),
         mEventHub(eventHub),
         mPolicy(policy),
-        mQueuedListener(listener),
+        mNextListener(listener),
         mGlobalMetaState(AMETA_NONE),
         mLedMetaState(AMETA_NONE),
         mGeneration(1),
@@ -140,7 +140,7 @@
         mReaderIsAliveCondition.notify_all();
 
         if (!events.empty()) {
-            notifyArgs += processEventsLocked(events.data(), events.size());
+            mPendingArgs += processEventsLocked(events.data(), events.size());
         }
 
         if (mNextTimeout != LLONG_MAX) {
@@ -150,16 +150,18 @@
                     ALOGD("Timeout expired, latency=%0.3fms", (now - mNextTimeout) * 0.000001f);
                 }
                 mNextTimeout = LLONG_MAX;
-                notifyArgs += timeoutExpiredLocked(now);
+                mPendingArgs += timeoutExpiredLocked(now);
             }
         }
 
         if (oldGeneration != mGeneration) {
             inputDevicesChanged = true;
             inputDevices = getInputDevicesLocked();
-            notifyArgs.emplace_back(
+            mPendingArgs.emplace_back(
                     NotifyInputDevicesChangedArgs{mContext.getNextId(), inputDevices});
         }
+
+        std::swap(notifyArgs, mPendingArgs);
     } // release lock
 
     // Send out a message that the describes the changed input devices.
@@ -175,8 +177,6 @@
         }
     }
 
-    notifyAll(std::move(notifyArgs));
-
     // Flush queued events out to the listener.
     // This must happen outside of the lock because the listener could potentially call
     // back into the InputReader's methods, such as getScanCodeState, or become blocked
@@ -184,7 +184,9 @@
     // resulting in a deadlock.  This situation is actually quite plausible because the
     // listener is actually the input dispatcher, which calls into the window manager,
     // which occasionally calls into the input reader.
-    mQueuedListener.flush();
+    for (const NotifyArgs& args : notifyArgs) {
+        mNextListener.notify(args);
+    }
 }
 
 std::list<NotifyArgs> InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {
@@ -234,10 +236,10 @@
     }
 
     InputDeviceIdentifier identifier = mEventHub->getDeviceIdentifier(eventHubId);
-    std::shared_ptr<InputDevice> device = createDeviceLocked(eventHubId, identifier);
+    std::shared_ptr<InputDevice> device = createDeviceLocked(when, eventHubId, identifier);
 
-    notifyAll(device->configure(when, mConfig, /*changes=*/{}));
-    notifyAll(device->reset(when));
+    mPendingArgs += device->configure(when, mConfig, /*changes=*/{});
+    mPendingArgs += device->reset(when);
 
     if (device->isIgnored()) {
         ALOGI("Device added: id=%d, eventHubId=%d, name='%s', descriptor='%s' "
@@ -310,16 +312,14 @@
         notifyExternalStylusPresenceChangedLocked();
     }
 
-    std::list<NotifyArgs> resetEvents;
     if (device->hasEventHubDevices()) {
-        resetEvents += device->configure(when, mConfig, /*changes=*/{});
+        mPendingArgs += device->configure(when, mConfig, /*changes=*/{});
     }
-    resetEvents += device->reset(when);
-    notifyAll(std::move(resetEvents));
+    mPendingArgs += device->reset(when);
 }
 
 std::shared_ptr<InputDevice> InputReader::createDeviceLocked(
-        int32_t eventHubId, const InputDeviceIdentifier& identifier) {
+        nsecs_t when, int32_t eventHubId, const InputDeviceIdentifier& identifier) {
     auto deviceIt = std::find_if(mDevices.begin(), mDevices.end(), [identifier](auto& devicePair) {
         const InputDeviceIdentifier identifier2 =
                 devicePair.second->getDeviceInfo().getIdentifier();
@@ -334,7 +334,7 @@
         device = std::make_shared<InputDevice>(&mContext, deviceId, bumpGenerationLocked(),
                                                identifier);
     }
-    device->addEventHubDevice(eventHubId, mConfig);
+    mPendingArgs += device->addEventHubDevice(when, eventHubId, mConfig);
     return device;
 }
 
@@ -387,7 +387,7 @@
     updateGlobalMetaStateLocked();
 
     // Enqueue configuration changed.
-    mQueuedListener.notifyConfigurationChanged({mContext.getNextId(), when});
+    mPendingArgs.emplace_back(NotifyConfigurationChangedArgs{mContext.getNextId(), when});
 }
 
 void InputReader::refreshConfigurationLocked(ConfigurationChanges changes) {
@@ -409,7 +409,7 @@
     } else {
         for (auto& devicePair : mDevices) {
             std::shared_ptr<InputDevice>& device = devicePair.second;
-            notifyAll(device->configure(now, mConfig, changes));
+            mPendingArgs += device->configure(now, mConfig, changes);
         }
     }
 
@@ -419,18 +419,13 @@
                   "There was no change in the pointer capture state.");
         } else {
             mCurrentPointerCaptureRequest = mConfig.pointerCaptureRequest;
-            mQueuedListener.notifyPointerCaptureChanged(
-                    {mContext.getNextId(), now, mCurrentPointerCaptureRequest});
+            mPendingArgs.emplace_back(
+                    NotifyPointerCaptureChangedArgs{mContext.getNextId(), now,
+                                                    mCurrentPointerCaptureRequest});
         }
     }
 }
 
-void InputReader::notifyAll(std::list<NotifyArgs>&& argsList) {
-    for (const NotifyArgs& args : argsList) {
-        mQueuedListener.notify(args);
-    }
-}
-
 void InputReader::updateGlobalMetaStateLocked() {
     mGlobalMetaState = 0;
 
@@ -690,7 +685,7 @@
 
     InputDevice* device = findInputDeviceLocked(deviceId);
     if (device) {
-        notifyAll(device->vibrate(sequence, repeat, token));
+        mPendingArgs += device->vibrate(sequence, repeat, token);
     }
 }
 
@@ -699,7 +694,7 @@
 
     InputDevice* device = findInputDeviceLocked(deviceId);
     if (device) {
-        notifyAll(device->cancelVibrate(token));
+        mPendingArgs += device->cancelVibrate(token);
     }
 }
 
diff --git a/services/inputflinger/reader/include/InputDevice.h b/services/inputflinger/reader/include/InputDevice.h
index 1cbcbf4..31dcb2e 100644
--- a/services/inputflinger/reader/include/InputDevice.h
+++ b/services/inputflinger/reader/include/InputDevice.h
@@ -77,11 +77,11 @@
     inline bool isIgnored() { return !getMapperCount() && !mController; }
 
     bool isEnabled();
-    [[nodiscard]] std::list<NotifyArgs> setEnabled(bool enabled, nsecs_t when);
 
     void dump(std::string& dump, const std::string& eventHubDevStr);
     void addEmptyEventHubDevice(int32_t eventHubId);
-    void addEventHubDevice(int32_t eventHubId, const InputReaderConfiguration& readerConfig);
+    [[nodiscard]] std::list<NotifyArgs> addEventHubDevice(
+            nsecs_t when, int32_t eventHubId, const InputReaderConfiguration& readerConfig);
     void removeEventHubDevice(int32_t eventHubId);
     [[nodiscard]] std::list<NotifyArgs> configure(nsecs_t when,
                                                   const InputReaderConfiguration& readerConfig,
@@ -206,6 +206,13 @@
     std::vector<std::unique_ptr<InputMapper>> createMappers(
             InputDeviceContext& contextPtr, const InputReaderConfiguration& readerConfig);
 
+    [[nodiscard]] std::list<NotifyArgs> configureInternal(
+            nsecs_t when, const InputReaderConfiguration& readerConfig,
+            ConfigurationChanges changes, bool forceEnable = false);
+
+    [[nodiscard]] std::list<NotifyArgs> updateEnableState(
+            nsecs_t when, const InputReaderConfiguration& readerConfig, bool forceEnable = false);
+
     PropertyMap mConfiguration;
 
     // Runs logic post a `process` call. This can be used to update the generated `NotifyArgs` as
diff --git a/services/inputflinger/reader/include/InputReader.h b/services/inputflinger/reader/include/InputReader.h
index 01ec7c1..9a297c9 100644
--- a/services/inputflinger/reader/include/InputReader.h
+++ b/services/inputflinger/reader/include/InputReader.h
@@ -121,7 +121,7 @@
 
 protected:
     // These members are protected so they can be instrumented by test cases.
-    virtual std::shared_ptr<InputDevice> createDeviceLocked(int32_t deviceId,
+    virtual std::shared_ptr<InputDevice> createDeviceLocked(nsecs_t when, int32_t deviceId,
                                                             const InputDeviceIdentifier& identifier)
             REQUIRES(mLock);
 
@@ -174,7 +174,14 @@
     // in parallel to passing it to the InputReader.
     std::shared_ptr<EventHubInterface> mEventHub;
     sp<InputReaderPolicyInterface> mPolicy;
-    QueuedInputListener mQueuedListener;
+
+    // The next stage that should receive the events generated inside InputReader.
+    InputListenerInterface& mNextListener;
+    // As various events are generated inside InputReader, they are stored inside this list. The
+    // list can only be accessed with the lock, so the events inside it are well-ordered.
+    // Once the reader is done working, these events will be swapped into a temporary storage and
+    // sent to the 'mNextListener' without holding the lock.
+    std::list<NotifyArgs> mPendingArgs GUARDED_BY(mLock);
 
     InputReaderConfiguration mConfig GUARDED_BY(mLock);
 
@@ -242,8 +249,6 @@
     ConfigurationChanges mConfigurationChangesToRefresh GUARDED_BY(mLock);
     void refreshConfigurationLocked(ConfigurationChanges changes) REQUIRES(mLock);
 
-    void notifyAll(std::list<NotifyArgs>&& argsList);
-
     PointerCaptureRequest mCurrentPointerCaptureRequest GUARDED_BY(mLock);
 
     // state queries
diff --git a/services/inputflinger/reader/mapper/MultiTouchInputMapper.h b/services/inputflinger/reader/mapper/MultiTouchInputMapper.h
index f300ee1..1d788df 100644
--- a/services/inputflinger/reader/mapper/MultiTouchInputMapper.h
+++ b/services/inputflinger/reader/mapper/MultiTouchInputMapper.h
@@ -27,8 +27,6 @@
     friend std::unique_ptr<T> createInputMapper(InputDeviceContext& deviceContext,
                                                 const InputReaderConfiguration& readerConfig,
                                                 Args... args);
-    explicit MultiTouchInputMapper(InputDeviceContext& deviceContext,
-                                   const InputReaderConfiguration& readerConfig);
 
     ~MultiTouchInputMapper() override;
 
@@ -41,6 +39,8 @@
     bool hasStylus() const override;
 
 private:
+    explicit MultiTouchInputMapper(InputDeviceContext& deviceContext,
+                                   const InputReaderConfiguration& readerConfig);
     // simulate_stylus_with_touch is a debug mode that converts all finger pointers reported by this
     // mapper's touchscreen into stylus pointers, and adds SOURCE_STYLUS to the input device.
     // It is used to simulate stylus events for debugging and testing on a device that does not
diff --git a/services/inputflinger/reader/mapper/SingleTouchInputMapper.h b/services/inputflinger/reader/mapper/SingleTouchInputMapper.h
index dac53cf..7726bfb 100644
--- a/services/inputflinger/reader/mapper/SingleTouchInputMapper.h
+++ b/services/inputflinger/reader/mapper/SingleTouchInputMapper.h
@@ -27,8 +27,6 @@
     friend std::unique_ptr<T> createInputMapper(InputDeviceContext& deviceContext,
                                                 const InputReaderConfiguration& readerConfig,
                                                 Args... args);
-    explicit SingleTouchInputMapper(InputDeviceContext& deviceContext,
-                                    const InputReaderConfiguration& readerConfig);
 
     ~SingleTouchInputMapper() override;
 
@@ -42,6 +40,8 @@
 
 private:
     SingleTouchMotionAccumulator mSingleTouchMotionAccumulator;
+    explicit SingleTouchInputMapper(InputDeviceContext& deviceContext,
+                                    const InputReaderConfiguration& readerConfig);
 };
 
 } // namespace android
diff --git a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
index eca0f86..6ea004d 100644
--- a/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchpadInputMapper.cpp
@@ -33,6 +33,7 @@
 #include <statslog.h>
 #include "TouchCursorInputMapperCommon.h"
 #include "TouchpadInputMapper.h"
+#include "gestures/HardwareProperties.h"
 #include "ui/Rotation.h"
 
 namespace android {
@@ -119,57 +120,6 @@
     return output;
 }
 
-short getMaxTouchCount(const InputDeviceContext& context) {
-    if (context.hasScanCode(BTN_TOOL_QUINTTAP)) return 5;
-    if (context.hasScanCode(BTN_TOOL_QUADTAP)) return 4;
-    if (context.hasScanCode(BTN_TOOL_TRIPLETAP)) return 3;
-    if (context.hasScanCode(BTN_TOOL_DOUBLETAP)) return 2;
-    if (context.hasScanCode(BTN_TOOL_FINGER)) return 1;
-    return 0;
-}
-
-HardwareProperties createHardwareProperties(const InputDeviceContext& context) {
-    HardwareProperties props;
-    RawAbsoluteAxisInfo absMtPositionX;
-    context.getAbsoluteAxisInfo(ABS_MT_POSITION_X, &absMtPositionX);
-    props.left = absMtPositionX.minValue;
-    props.right = absMtPositionX.maxValue;
-    props.res_x = absMtPositionX.resolution;
-
-    RawAbsoluteAxisInfo absMtPositionY;
-    context.getAbsoluteAxisInfo(ABS_MT_POSITION_Y, &absMtPositionY);
-    props.top = absMtPositionY.minValue;
-    props.bottom = absMtPositionY.maxValue;
-    props.res_y = absMtPositionY.resolution;
-
-    RawAbsoluteAxisInfo absMtOrientation;
-    context.getAbsoluteAxisInfo(ABS_MT_ORIENTATION, &absMtOrientation);
-    props.orientation_minimum = absMtOrientation.minValue;
-    props.orientation_maximum = absMtOrientation.maxValue;
-
-    RawAbsoluteAxisInfo absMtSlot;
-    context.getAbsoluteAxisInfo(ABS_MT_SLOT, &absMtSlot);
-    props.max_finger_cnt = absMtSlot.maxValue - absMtSlot.minValue + 1;
-    props.max_touch_cnt = getMaxTouchCount(context);
-
-    // T5R2 ("Track 5, Report 2") is a feature of some old Synaptics touchpads that could track 5
-    // fingers but only report the coordinates of 2 of them. We don't know of any external touchpads
-    // that did this, so assume false.
-    props.supports_t5r2 = false;
-
-    props.support_semi_mt = context.hasInputProperty(INPUT_PROP_SEMI_MT);
-    props.is_button_pad = context.hasInputProperty(INPUT_PROP_BUTTONPAD);
-
-    // Mouse-only properties, which will always be false.
-    props.has_wheel = false;
-    props.wheel_is_hi_res = false;
-
-    // Linux Kernel haptic touchpad support isn't merged yet, so for now assume that no touchpads
-    // are haptic.
-    props.is_haptic_pad = false;
-    return props;
-}
-
 void gestureInterpreterCallback(void* clientData, const Gesture* gesture) {
     TouchpadInputMapper* mapper = static_cast<TouchpadInputMapper*>(clientData);
     mapper->consumeGesture(gesture);
diff --git a/services/inputflinger/reader/mapper/gestures/HardwareProperties.cpp b/services/inputflinger/reader/mapper/gestures/HardwareProperties.cpp
new file mode 100644
index 0000000..04655dc
--- /dev/null
+++ b/services/inputflinger/reader/mapper/gestures/HardwareProperties.cpp
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "HardwareProperties.h"
+
+namespace android {
+
+namespace {
+
+unsigned short getMaxTouchCount(const InputDeviceContext& context) {
+    if (context.hasScanCode(BTN_TOOL_QUINTTAP)) return 5;
+    if (context.hasScanCode(BTN_TOOL_QUADTAP)) return 4;
+    if (context.hasScanCode(BTN_TOOL_TRIPLETAP)) return 3;
+    if (context.hasScanCode(BTN_TOOL_DOUBLETAP)) return 2;
+    if (context.hasScanCode(BTN_TOOL_FINGER)) return 1;
+    return 0;
+}
+
+} // namespace
+
+HardwareProperties createHardwareProperties(const InputDeviceContext& context) {
+    HardwareProperties props;
+    RawAbsoluteAxisInfo absMtPositionX;
+    context.getAbsoluteAxisInfo(ABS_MT_POSITION_X, &absMtPositionX);
+    props.left = absMtPositionX.minValue;
+    props.right = absMtPositionX.maxValue;
+    props.res_x = absMtPositionX.resolution;
+
+    RawAbsoluteAxisInfo absMtPositionY;
+    context.getAbsoluteAxisInfo(ABS_MT_POSITION_Y, &absMtPositionY);
+    props.top = absMtPositionY.minValue;
+    props.bottom = absMtPositionY.maxValue;
+    props.res_y = absMtPositionY.resolution;
+
+    RawAbsoluteAxisInfo absMtOrientation;
+    context.getAbsoluteAxisInfo(ABS_MT_ORIENTATION, &absMtOrientation);
+    props.orientation_minimum = absMtOrientation.minValue;
+    props.orientation_maximum = absMtOrientation.maxValue;
+
+    RawAbsoluteAxisInfo absMtSlot;
+    context.getAbsoluteAxisInfo(ABS_MT_SLOT, &absMtSlot);
+    props.max_finger_cnt = absMtSlot.maxValue - absMtSlot.minValue + 1;
+    props.max_touch_cnt = getMaxTouchCount(context);
+
+    // T5R2 ("Track 5, Report 2") is a feature of some old Synaptics touchpads that could track 5
+    // fingers but only report the coordinates of 2 of them. We don't know of any external touchpads
+    // that did this, so assume false.
+    props.supports_t5r2 = false;
+
+    props.support_semi_mt = context.hasInputProperty(INPUT_PROP_SEMI_MT);
+    props.is_button_pad = context.hasInputProperty(INPUT_PROP_BUTTONPAD);
+
+    // Mouse-only properties, which will always be false.
+    props.has_wheel = false;
+    props.wheel_is_hi_res = false;
+
+    // Linux Kernel haptic touchpad support isn't merged yet, so for now assume that no touchpads
+    // are haptic.
+    props.is_haptic_pad = false;
+
+    RawAbsoluteAxisInfo absMtPressure;
+    context.getAbsoluteAxisInfo(ABS_MT_PRESSURE, &absMtPressure);
+    props.reports_pressure = absMtPressure.valid;
+    return props;
+}
+
+} // namespace android
diff --git a/libs/ui/include_types/ui/DataspaceUtils.h b/services/inputflinger/reader/mapper/gestures/HardwareProperties.h
similarity index 67%
rename from libs/ui/include_types/ui/DataspaceUtils.h
rename to services/inputflinger/reader/mapper/gestures/HardwareProperties.h
index a461cb4..672f8c1 100644
--- a/libs/ui/include_types/ui/DataspaceUtils.h
+++ b/services/inputflinger/reader/mapper/gestures/HardwareProperties.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -16,14 +16,14 @@
 
 #pragma once
 
-#include <ui/GraphicTypes.h>
+#include "InputDevice.h"
+
+#include "include/gestures.h"
 
 namespace android {
 
-inline bool isHdrDataspace(ui::Dataspace dataspace) {
-    const auto transfer = dataspace & HAL_DATASPACE_TRANSFER_MASK;
+// Creates a Gestures library HardwareProperties struct for the touchpad described by the
+// InputDeviceContext.
+HardwareProperties createHardwareProperties(const InputDeviceContext& context);
 
-    return transfer == HAL_DATASPACE_TRANSFER_ST2084 || transfer == HAL_DATASPACE_TRANSFER_HLG;
-}
-
-} // namespace android
\ No newline at end of file
+} // namespace android
diff --git a/services/inputflinger/rust/Android.bp b/services/inputflinger/rust/Android.bp
new file mode 100644
index 0000000..23c1691
--- /dev/null
+++ b/services/inputflinger/rust/Android.bp
@@ -0,0 +1,52 @@
+// 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.
+
+// Generate the C++ code that Rust calls into.
+genrule {
+    name: "inputflinger_rs_bootstrap_bridge_code",
+    tools: ["cxxbridge"],
+    cmd: "$(location cxxbridge) $(in) >> $(out)",
+    srcs: ["lib.rs"],
+    out: ["inputflinger_rs_bootstrap_cxx_generated.cc"],
+}
+
+// Generate a C++ header containing the C++ bindings
+// to the Rust exported functions in lib.rs.
+genrule {
+    name: "inputflinger_rs_bootstrap_bridge_header",
+    tools: ["cxxbridge"],
+    cmd: "$(location cxxbridge) $(in) --header >> $(out)",
+    srcs: ["lib.rs"],
+    out: ["inputflinger_bootstrap.rs.h"],
+}
+
+rust_ffi_static {
+    name: "libinputflinger_rs",
+    crate_name: "inputflinger",
+    srcs: ["lib.rs"],
+    rustlibs: [
+        "libcxx",
+        "com.android.server.inputflinger-rust",
+        "libbinder_rs",
+        "liblog_rust",
+        "liblogger",
+    ],
+    host_supported: true,
+}
+
+cc_library_headers {
+    name: "inputflinger_rs_bootstrap_cxx_headers",
+    host_supported: true,
+    export_include_dirs: ["ffi"],
+}
diff --git a/libs/ui/include_types/ui/DataspaceUtils.h b/services/inputflinger/rust/ffi/InputFlingerBootstrap.h
similarity index 62%
copy from libs/ui/include_types/ui/DataspaceUtils.h
copy to services/inputflinger/rust/ffi/InputFlingerBootstrap.h
index a461cb4..eb79ab5 100644
--- a/libs/ui/include_types/ui/DataspaceUtils.h
+++ b/services/inputflinger/rust/ffi/InputFlingerBootstrap.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2021 The Android Open Source Project
+ * 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.
@@ -16,14 +16,6 @@
 
 #pragma once
 
-#include <ui/GraphicTypes.h>
+#include <android/binder_parcel.h>
 
-namespace android {
-
-inline bool isHdrDataspace(ui::Dataspace dataspace) {
-    const auto transfer = dataspace & HAL_DATASPACE_TRANSFER_MASK;
-
-    return transfer == HAL_DATASPACE_TRANSFER_ST2084 || transfer == HAL_DATASPACE_TRANSFER_HLG;
-}
-
-} // namespace android
\ No newline at end of file
+using IInputFlingerRustBootstrapCallbackAIBinder = AIBinder;
diff --git a/services/inputflinger/rust/lib.rs b/services/inputflinger/rust/lib.rs
new file mode 100644
index 0000000..501e435
--- /dev/null
+++ b/services/inputflinger/rust/lib.rs
@@ -0,0 +1,102 @@
+/*
+ * 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.
+ */
+
+//! # The rust component of InputFlinger
+//!
+//! We use cxxbridge to create IInputFlingerRust - the Rust component of inputflinger - and
+//! pass it back to C++ as a local AIDL interface.
+
+use binder::{
+    unstable_api::{AIBinder, new_spibinder,},
+    BinderFeatures, Interface, StatusCode, Strong,
+};
+use com_android_server_inputflinger::aidl::com::android::server::inputflinger::IInputFlingerRust::{
+    BnInputFlingerRust, IInputFlingerRust,
+    IInputFlingerRustBootstrapCallback::IInputFlingerRustBootstrapCallback,
+};
+use log::debug;
+
+const LOG_TAG: &str = "inputflinger_bootstrap";
+
+#[cxx::bridge]
+#[allow(unsafe_op_in_unsafe_fn)]
+mod ffi {
+    extern "C++" {
+        include!("InputFlingerBootstrap.h");
+        type IInputFlingerRustBootstrapCallbackAIBinder;
+    }
+
+    extern "Rust" {
+        unsafe fn create_inputflinger_rust(
+            callback: *mut IInputFlingerRustBootstrapCallbackAIBinder,
+        );
+    }
+}
+
+/// Create the IInputFlingerRust implementation.
+/// This is the singular entry point from C++ into Rust.
+/// The `callback` parameter must be a valid pointer to an AIBinder implementation of
+/// the `IInputFlingerRustBootstrapCallback` interface. The IInputFlingerRust implementation that
+/// is created will be passed back through the callback from within this function.
+/// NOTE: This function must not hold a strong reference to the callback beyond its scope.
+///
+/// # Safety
+///
+/// This function is safe iff `callback` is a valid pointer to an `AIBinder` interface of type
+/// `IInputFlingerRustBootstrapCallback`. The pointer must have had its reference count manually
+/// incremented using `AIBinder_incStrong`. See `binder::unstable_api::new_spibinder`.
+unsafe fn create_inputflinger_rust(callback: *mut ffi::IInputFlingerRustBootstrapCallbackAIBinder) {
+    logger::init(
+        logger::Config::default().with_tag_on_device(LOG_TAG).with_min_level(log::Level::Trace),
+    );
+
+    let callback = callback as *mut AIBinder;
+    if callback.is_null() {
+        panic!("create_inputflinger_rust cannot be called with a null callback");
+    }
+
+    // SAFETY: Our caller guaranteed that `callback` is a valid pointer to an `AIBinder` and its
+    // reference count has been incremented..
+    let Some(callback) = (unsafe { new_spibinder(callback) }) else {
+            panic!("Failed to get SpAIBinder from raw callback pointer");
+        };
+
+    let callback: Result<Strong<dyn IInputFlingerRustBootstrapCallback>, StatusCode> =
+        callback.into_interface();
+    match callback {
+        Ok(callback) => {
+            debug!("Creating InputFlingerRust");
+            let service =
+                BnInputFlingerRust::new_binder(InputFlingerRust {}, BinderFeatures::default());
+            callback.onProvideInputFlingerRust(&service).unwrap();
+        }
+        Err(status) => {
+            panic!("Failed to convert AIBinder into the callback interface: {}", status);
+        }
+    }
+}
+
+struct InputFlingerRust {}
+
+impl Interface for InputFlingerRust {}
+
+impl IInputFlingerRust for InputFlingerRust {}
+
+impl Drop for InputFlingerRust {
+    fn drop(&mut self) {
+        debug!("Destroying InputFlingerRust");
+    }
+}
diff --git a/services/inputflinger/tests/Android.bp b/services/inputflinger/tests/Android.bp
index 3d6df30..6410046 100644
--- a/services/inputflinger/tests/Android.bp
+++ b/services/inputflinger/tests/Android.bp
@@ -47,6 +47,7 @@
         "FakePointerController.cpp",
         "FocusResolver_test.cpp",
         "GestureConverter_test.cpp",
+        "HardwareProperties_test.cpp",
         "HardwareStateConverter_test.cpp",
         "InputDeviceMetricsCollector_test.cpp",
         "InputMapperTest.cpp",
@@ -57,6 +58,7 @@
         "InstrumentedInputReader.cpp",
         "LatencyTracker_test.cpp",
         "NotifyArgs_test.cpp",
+        "PointerChoreographer_test.cpp",
         "PreferStylusOverTouch_test.cpp",
         "PropertyProvider_test.cpp",
         "SlopController_test.cpp",
diff --git a/services/inputflinger/tests/FakeInputReaderPolicy.cpp b/services/inputflinger/tests/FakeInputReaderPolicy.cpp
index 78420c0..41c98ef 100644
--- a/services/inputflinger/tests/FakeInputReaderPolicy.cpp
+++ b/services/inputflinger/tests/FakeInputReaderPolicy.cpp
@@ -158,7 +158,8 @@
     return mConfig;
 }
 
-const std::vector<InputDeviceInfo>& FakeInputReaderPolicy::getInputDevices() const {
+const std::vector<InputDeviceInfo> FakeInputReaderPolicy::getInputDevices() const {
+    std::scoped_lock lock(mLock);
     return mInputDevices;
 }
 
@@ -228,7 +229,7 @@
 
 void FakeInputReaderPolicy::notifyInputDevicesChanged(
         const std::vector<InputDeviceInfo>& inputDevices) {
-    std::scoped_lock<std::mutex> lock(mLock);
+    std::scoped_lock lock(mLock);
     mInputDevices = inputDevices;
     mInputDevicesChanged = true;
     mDevicesChangedCondition.notify_all();
@@ -256,7 +257,7 @@
 }
 
 void FakeInputReaderPolicy::notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) {
-    std::scoped_lock<std::mutex> lock(mLock);
+    std::scoped_lock lock(mLock);
     mStylusGestureNotified = deviceId;
 }
 
diff --git a/services/inputflinger/tests/FakeInputReaderPolicy.h b/services/inputflinger/tests/FakeInputReaderPolicy.h
index e03d28d..48912a6 100644
--- a/services/inputflinger/tests/FakeInputReaderPolicy.h
+++ b/services/inputflinger/tests/FakeInputReaderPolicy.h
@@ -64,7 +64,7 @@
     void removeDisabledDevice(int32_t deviceId);
     void setPointerController(std::shared_ptr<FakePointerController> controller);
     const InputReaderConfiguration& getReaderConfiguration() const;
-    const std::vector<InputDeviceInfo>& getInputDevices() const;
+    const std::vector<InputDeviceInfo> getInputDevices() const;
     TouchAffineTransformation getTouchAffineTransformation(const std::string& inputDeviceDescriptor,
                                                            ui::Rotation surfaceRotation);
     void setTouchAffineTransformation(const TouchAffineTransformation t);
@@ -91,7 +91,7 @@
     void waitForInputDevices(std::function<void(bool)> processDevicesChanged);
     void notifyStylusGestureStarted(int32_t deviceId, nsecs_t eventTime) override;
 
-    std::mutex mLock;
+    mutable std::mutex mLock;
     std::condition_variable mDevicesChangedCondition;
 
     InputReaderConfiguration mConfig;
diff --git a/services/inputflinger/tests/HardwareProperties_test.cpp b/services/inputflinger/tests/HardwareProperties_test.cpp
new file mode 100644
index 0000000..8dfa8c8
--- /dev/null
+++ b/services/inputflinger/tests/HardwareProperties_test.cpp
@@ -0,0 +1,165 @@
+/*
+ * 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/HardwareProperties.h>
+
+#include <memory>
+#include <set>
+
+#include <gtest/gtest.h>
+#include <linux/input-event-codes.h>
+
+#include "EventHub.h"
+#include "InputDevice.h"
+#include "InterfaceMocks.h"
+#include "TestConstants.h"
+#include "include/gestures.h"
+
+namespace android {
+
+using testing::Return;
+
+class HardwarePropertiesTest : public testing::Test {
+public:
+    HardwarePropertiesTest() {
+        EXPECT_CALL(mMockInputReaderContext, getEventHub()).WillRepeatedly(Return(&mMockEventHub));
+        InputDeviceIdentifier identifier;
+        identifier.name = "device";
+        identifier.location = "USB1";
+        mDevice = std::make_unique<InputDevice>(&mMockInputReaderContext, DEVICE_ID,
+                                                /*generation=*/2, identifier);
+        mDeviceContext = std::make_unique<InputDeviceContext>(*mDevice, EVENTHUB_ID);
+    }
+
+protected:
+    static constexpr int32_t DEVICE_ID = END_RESERVED_ID + 1000;
+    static constexpr int32_t EVENTHUB_ID = 1;
+
+    void setupValidAxis(int axis, int32_t min, int32_t max, int32_t resolution) {
+        EXPECT_CALL(mMockEventHub, getAbsoluteAxisInfo(EVENTHUB_ID, axis, testing::_))
+                .WillRepeatedly([=](int32_t, int32_t, RawAbsoluteAxisInfo* outAxisInfo) {
+                    outAxisInfo->valid = true;
+                    outAxisInfo->minValue = min;
+                    outAxisInfo->maxValue = max;
+                    outAxisInfo->flat = 0;
+                    outAxisInfo->fuzz = 0;
+                    outAxisInfo->resolution = resolution;
+                    return OK;
+                });
+    }
+
+    void setupInvalidAxis(int axis) {
+        EXPECT_CALL(mMockEventHub, getAbsoluteAxisInfo(EVENTHUB_ID, axis, testing::_))
+                .WillRepeatedly([=](int32_t, int32_t, RawAbsoluteAxisInfo* outAxisInfo) {
+                    outAxisInfo->valid = false;
+                    return -1;
+                });
+    }
+
+    void setProperty(int property, bool value) {
+        EXPECT_CALL(mMockEventHub, hasInputProperty(EVENTHUB_ID, property))
+                .WillRepeatedly(Return(value));
+    }
+
+    void setButtonsPresent(std::set<int> buttonCodes, bool present) {
+        for (const auto& buttonCode : buttonCodes) {
+            EXPECT_CALL(mMockEventHub, hasScanCode(EVENTHUB_ID, buttonCode))
+                    .WillRepeatedly(Return(present));
+        }
+    }
+
+    MockEventHubInterface mMockEventHub;
+    MockInputReaderContext mMockInputReaderContext;
+    std::unique_ptr<InputDevice> mDevice;
+    std::unique_ptr<InputDeviceContext> mDeviceContext;
+};
+
+TEST_F(HardwarePropertiesTest, FancyTouchpad) {
+    setupValidAxis(ABS_MT_POSITION_X, 0, 2048, 27);
+    setupValidAxis(ABS_MT_POSITION_Y, 0, 1500, 30);
+    setupValidAxis(ABS_MT_ORIENTATION, -3, 4, 0);
+    setupValidAxis(ABS_MT_SLOT, 0, 15, 0);
+    setupValidAxis(ABS_MT_PRESSURE, 0, 256, 0);
+
+    setProperty(INPUT_PROP_SEMI_MT, false);
+    setProperty(INPUT_PROP_BUTTONPAD, true);
+
+    setButtonsPresent({BTN_TOOL_FINGER, BTN_TOOL_DOUBLETAP, BTN_TOOL_TRIPLETAP, BTN_TOOL_QUADTAP,
+                       BTN_TOOL_QUINTTAP},
+                      true);
+
+    HardwareProperties hwprops = createHardwareProperties(*mDeviceContext);
+    EXPECT_NEAR(0, hwprops.left, EPSILON);
+    EXPECT_NEAR(0, hwprops.top, EPSILON);
+    EXPECT_NEAR(2048, hwprops.right, EPSILON);
+    EXPECT_NEAR(1500, hwprops.bottom, EPSILON);
+
+    EXPECT_NEAR(27, hwprops.res_x, EPSILON);
+    EXPECT_NEAR(30, hwprops.res_y, EPSILON);
+
+    EXPECT_NEAR(-3, hwprops.orientation_minimum, EPSILON);
+    EXPECT_NEAR(4, hwprops.orientation_maximum, EPSILON);
+
+    EXPECT_EQ(16, hwprops.max_finger_cnt);
+    EXPECT_EQ(5, hwprops.max_touch_cnt);
+
+    EXPECT_FALSE(hwprops.supports_t5r2);
+    EXPECT_FALSE(hwprops.support_semi_mt);
+    EXPECT_TRUE(hwprops.is_button_pad);
+    EXPECT_FALSE(hwprops.has_wheel);
+    EXPECT_FALSE(hwprops.wheel_is_hi_res);
+    EXPECT_FALSE(hwprops.is_haptic_pad);
+    EXPECT_TRUE(hwprops.reports_pressure);
+}
+
+TEST_F(HardwarePropertiesTest, BasicTouchpad) {
+    setupValidAxis(ABS_MT_POSITION_X, 0, 1024, 0);
+    setupValidAxis(ABS_MT_POSITION_Y, 0, 768, 0);
+    setupValidAxis(ABS_MT_SLOT, 0, 7, 0);
+
+    setupInvalidAxis(ABS_MT_ORIENTATION);
+    setupInvalidAxis(ABS_MT_PRESSURE);
+
+    setProperty(INPUT_PROP_SEMI_MT, false);
+    setProperty(INPUT_PROP_BUTTONPAD, false);
+
+    setButtonsPresent({BTN_TOOL_FINGER, BTN_TOOL_DOUBLETAP, BTN_TOOL_TRIPLETAP}, true);
+    setButtonsPresent({BTN_TOOL_QUADTAP, BTN_TOOL_QUINTTAP}, false);
+
+    HardwareProperties hwprops = createHardwareProperties(*mDeviceContext);
+    EXPECT_NEAR(0, hwprops.left, EPSILON);
+    EXPECT_NEAR(0, hwprops.top, EPSILON);
+    EXPECT_NEAR(1024, hwprops.right, EPSILON);
+    EXPECT_NEAR(768, hwprops.bottom, EPSILON);
+
+    EXPECT_NEAR(0, hwprops.res_x, EPSILON);
+    EXPECT_NEAR(0, hwprops.res_y, EPSILON);
+
+    EXPECT_NEAR(0, hwprops.orientation_minimum, EPSILON);
+    EXPECT_NEAR(0, hwprops.orientation_maximum, EPSILON);
+
+    EXPECT_EQ(8, hwprops.max_finger_cnt);
+    EXPECT_EQ(3, hwprops.max_touch_cnt);
+
+    EXPECT_FALSE(hwprops.supports_t5r2);
+    EXPECT_FALSE(hwprops.support_semi_mt);
+    EXPECT_FALSE(hwprops.is_button_pad);
+    EXPECT_FALSE(hwprops.has_wheel);
+    EXPECT_FALSE(hwprops.wheel_is_hi_res);
+    EXPECT_FALSE(hwprops.is_haptic_pad);
+    EXPECT_FALSE(hwprops.reports_pressure);
+}
+
+} // namespace android
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index 6d9cd87..dc281a3 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -1131,10 +1131,7 @@
         mInfo.name = name;
         mInfo.dispatchingTimeout = DISPATCHING_TIMEOUT;
         mInfo.alpha = 1.0;
-        mInfo.frameLeft = 0;
-        mInfo.frameTop = 0;
-        mInfo.frameRight = WIDTH;
-        mInfo.frameBottom = HEIGHT;
+        mInfo.frame = Rect(0, 0, WIDTH, HEIGHT);
         mInfo.transform.set(0, 0);
         mInfo.globalScaleFactor = 1.0;
         mInfo.touchableRegion.clear();
@@ -1215,10 +1212,7 @@
     void setApplicationToken(sp<IBinder> token) { mInfo.applicationInfo.token = token; }
 
     void setFrame(const Rect& frame, const ui::Transform& displayTransform = ui::Transform()) {
-        mInfo.frameLeft = frame.left;
-        mInfo.frameTop = frame.top;
-        mInfo.frameRight = frame.right;
-        mInfo.frameBottom = frame.bottom;
+        mInfo.frame = frame;
         mInfo.touchableRegion.clear();
         mInfo.addTouchableRegion(frame);
 
@@ -9727,12 +9721,11 @@
                                           mPolicyFlags);
     }
 
-    sp<FakeWindowHandle> createWindow() const {
+    sp<FakeWindowHandle> createWindow(const char* name) const {
         std::shared_ptr<FakeApplicationHandle> overlayApplication =
                 std::make_shared<FakeApplicationHandle>();
-        sp<FakeWindowHandle> window =
-                sp<FakeWindowHandle>::make(overlayApplication, mDispatcher, "Owned Window",
-                                           ADISPLAY_ID_DEFAULT);
+        sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(overlayApplication, mDispatcher,
+                                                                 name, ADISPLAY_ID_DEFAULT);
         window->setOwnerInfo(mPid, mUid);
         return window;
     }
@@ -9742,7 +9735,7 @@
 
 TEST_F(InputDispatcherTargetedInjectionTest, CanInjectIntoOwnedWindow) {
     auto owner = User(mDispatcher, gui::Pid{10}, gui::Uid{11});
-    auto window = owner.createWindow();
+    auto window = owner.createWindow("Owned window");
     mDispatcher->onWindowInfosChanged({{*window->getInfo()}, {}, 0, 0});
 
     EXPECT_EQ(InputEventInjectionResult::SUCCEEDED,
@@ -9759,7 +9752,7 @@
 
 TEST_F(InputDispatcherTargetedInjectionTest, CannotInjectIntoUnownedWindow) {
     auto owner = User(mDispatcher, gui::Pid{10}, gui::Uid{11});
-    auto window = owner.createWindow();
+    auto window = owner.createWindow("Owned window");
     mDispatcher->onWindowInfosChanged({{*window->getInfo()}, {}, 0, 0});
 
     auto rando = User(mDispatcher, gui::Pid{20}, gui::Uid{21});
@@ -9776,8 +9769,8 @@
 
 TEST_F(InputDispatcherTargetedInjectionTest, CanInjectIntoOwnedSpyWindow) {
     auto owner = User(mDispatcher, gui::Pid{10}, gui::Uid{11});
-    auto window = owner.createWindow();
-    auto spy = owner.createWindow();
+    auto window = owner.createWindow("Owned window");
+    auto spy = owner.createWindow("Owned spy");
     spy->setSpy(true);
     spy->setTrustedOverlay(true);
     mDispatcher->onWindowInfosChanged({{*spy->getInfo(), *window->getInfo()}, {}, 0, 0});
@@ -9790,10 +9783,10 @@
 
 TEST_F(InputDispatcherTargetedInjectionTest, CannotInjectIntoUnownedSpyWindow) {
     auto owner = User(mDispatcher, gui::Pid{10}, gui::Uid{11});
-    auto window = owner.createWindow();
+    auto window = owner.createWindow("Owned window");
 
     auto rando = User(mDispatcher, gui::Pid{20}, gui::Uid{21});
-    auto randosSpy = rando.createWindow();
+    auto randosSpy = rando.createWindow("Rando's spy");
     randosSpy->setSpy(true);
     randosSpy->setTrustedOverlay(true);
     mDispatcher->onWindowInfosChanged({{*randosSpy->getInfo(), *window->getInfo()}, {}, 0, 0});
@@ -9808,10 +9801,10 @@
 
 TEST_F(InputDispatcherTargetedInjectionTest, CanInjectIntoAnyWindowWhenNotTargeting) {
     auto owner = User(mDispatcher, gui::Pid{10}, gui::Uid{11});
-    auto window = owner.createWindow();
+    auto window = owner.createWindow("Owned window");
 
     auto rando = User(mDispatcher, gui::Pid{20}, gui::Uid{21});
-    auto randosSpy = rando.createWindow();
+    auto randosSpy = rando.createWindow("Rando's spy");
     randosSpy->setSpy(true);
     randosSpy->setTrustedOverlay(true);
     mDispatcher->onWindowInfosChanged({{*randosSpy->getInfo(), *window->getInfo()}, {}, 0, 0});
@@ -9833,10 +9826,10 @@
 
 TEST_F(InputDispatcherTargetedInjectionTest, CannotGenerateActionOutsideToOtherUids) {
     auto owner = User(mDispatcher, gui::Pid{10}, gui::Uid{11});
-    auto window = owner.createWindow();
+    auto window = owner.createWindow("Owned window");
 
     auto rando = User(mDispatcher, gui::Pid{20}, gui::Uid{21});
-    auto randosWindow = rando.createWindow();
+    auto randosWindow = rando.createWindow("Rando's window");
     randosWindow->setFrame(Rect{-10, -10, -5, -5});
     randosWindow->setWatchOutsideTouch(true);
     mDispatcher->onWindowInfosChanged({{*randosWindow->getInfo(), *window->getInfo()}, {}, 0, 0});
diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp
index 1b19870..bce0937 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -1490,6 +1490,46 @@
             AllOf(UP, WithKeyCode(AKEYCODE_STYLUS_BUTTON_TERTIARY))));
 }
 
+TEST_F(InputReaderIntegrationTest, KeyboardWithStylusButtons) {
+    std::unique_ptr<UinputKeyboard> keyboard =
+            createUinputDevice<UinputKeyboard>("KeyboardWithStylusButtons", /*productId=*/99,
+                                               std::initializer_list<int>{KEY_Q, KEY_W, KEY_E,
+                                                                          KEY_R, KEY_T, KEY_Y,
+                                                                          BTN_STYLUS, BTN_STYLUS2,
+                                                                          BTN_STYLUS3});
+    ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertInputDevicesChanged());
+
+    const auto device = findDeviceByName(keyboard->getName());
+    ASSERT_TRUE(device.has_value());
+
+    // An alphabetical keyboard that reports stylus buttons should not be recognized as a stylus.
+    ASSERT_EQ(AINPUT_SOURCE_KEYBOARD, device->getSources())
+            << "Unexpected source " << inputEventSourceToString(device->getSources()).c_str();
+    ASSERT_EQ(AINPUT_KEYBOARD_TYPE_ALPHABETIC, device->getKeyboardType());
+}
+
+TEST_F(InputReaderIntegrationTest, HidUsageKeyboardIsNotAStylus) {
+    // Create a Uinput keyboard that simulates a keyboard that can report HID usage codes. The
+    // hid-input driver reports HID usage codes using the value for EV_MSC MSC_SCAN event.
+    std::unique_ptr<UinputKeyboardWithHidUsage> keyboard =
+            createUinputDevice<UinputKeyboardWithHidUsage>(
+                    std::initializer_list<int>{KEY_VOLUMEUP, KEY_VOLUMEDOWN});
+    ASSERT_NO_FATAL_FAILURE(mFakePolicy->assertInputDevicesChanged());
+
+    const auto device = findDeviceByName(keyboard->getName());
+    ASSERT_TRUE(device.has_value());
+
+    ASSERT_EQ(AINPUT_SOURCE_KEYBOARD, device->getSources())
+            << "Unexpected source " << inputEventSourceToString(device->getSources()).c_str();
+
+    // If a device supports reporting HID usage codes, it shouldn't automatically support
+    // stylus keys.
+    const std::vector<int> keycodes{AKEYCODE_STYLUS_BUTTON_PRIMARY};
+    uint8_t outFlags[] = {0};
+    ASSERT_TRUE(mReader->hasKeys(device->getId(), AINPUT_SOURCE_KEYBOARD, keycodes, outFlags));
+    ASSERT_EQ(0, outFlags[0]) << "Keyboard should not have stylus button";
+}
+
 /**
  * The Steam controller sends BTN_GEAR_DOWN and BTN_GEAR_UP for the two "paddle" buttons
  * on the back. In this test, we make sure that BTN_GEAR_DOWN / BTN_WHEEL and BTN_GEAR_UP
@@ -1829,6 +1869,38 @@
     ASSERT_EQ(AMOTION_EVENT_ACTION_UP, args.action);
 }
 
+/**
+ * Some drivers historically have reported axis values outside of the range specified in the
+ * evdev axis info. Ensure we don't crash when this happens. For example, a driver may report a
+ * pressure value greater than the reported maximum, since it unclear what specific meaning the
+ * maximum value for pressure has (beyond the maximum value that can be produced by a sensor),
+ * and no units for pressure (resolution) is specified by the evdev documentation.
+ */
+TEST_P(TouchIntegrationTest, AcceptsAxisValuesOutsideReportedRange) {
+    const Point centerPoint = mDevice->getCenterPoint();
+
+    // Down with pressure outside the reported range
+    mDevice->sendSlot(FIRST_SLOT);
+    mDevice->sendTrackingId(FIRST_TRACKING_ID);
+    mDevice->sendDown(centerPoint);
+    mDevice->sendPressure(UinputTouchScreen::RAW_PRESSURE_MAX + 2);
+    mDevice->sendSync();
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
+            WithMotionAction(AMOTION_EVENT_ACTION_DOWN)));
+
+    // Move to a point outside the reported range
+    mDevice->sendMove(Point(DISPLAY_WIDTH, DISPLAY_HEIGHT) + Point(1, 1));
+    mDevice->sendSync();
+    ASSERT_NO_FATAL_FAILURE(mTestListener->assertNotifyMotionWasCalled(
+            WithMotionAction(AMOTION_EVENT_ACTION_MOVE)));
+
+    // Up
+    mDevice->sendUp();
+    mDevice->sendSync();
+    ASSERT_NO_FATAL_FAILURE(
+            mTestListener->assertNotifyMotionWasCalled(WithMotionAction(AMOTION_EVENT_ACTION_UP)));
+}
+
 TEST_P(TouchIntegrationTest, NotifiesPolicyWhenStylusGestureStarted) {
     const Point centerPoint = mDevice->getCenterPoint();
 
@@ -2656,7 +2728,7 @@
 
 // A single input device is associated with a specific display. Check that:
 // 1. Device is disabled if the viewport corresponding to the associated display is not found
-// 2. Device is disabled when setEnabled API is called
+// 2. Device is disabled when configure API is called
 TEST_F(InputDeviceTest, Configure_AssignsDisplayPort) {
     mDevice->addMapper<FakeInputMapper>(EVENTHUB_ID, mFakePolicy->getReaderConfiguration(),
                                         AINPUT_SOURCE_TOUCHSCREEN);
@@ -2763,7 +2835,8 @@
     mFakeEventHub->addDevice(TEST_EVENTHUB_ID, "Test EventHub device", InputDeviceClass::BATTERY);
 
     InputDevice device(mReader->getContext(), /*id=*/1, /*generation=*/2, /*identifier=*/{});
-    device.addEventHubDevice(TEST_EVENTHUB_ID, mFakePolicy->getReaderConfiguration());
+    auto _ = device.addEventHubDevice(ARBITRARY_TIME, TEST_EVENTHUB_ID,
+                                      mFakePolicy->getReaderConfiguration());
     device.removeEventHubDevice(TEST_EVENTHUB_ID);
     std::string dumpStr, eventHubDevStr;
     device.dump(dumpStr, eventHubDevStr);
diff --git a/services/inputflinger/tests/InstrumentedInputReader.cpp b/services/inputflinger/tests/InstrumentedInputReader.cpp
index 1f8cd12..110ca5f 100644
--- a/services/inputflinger/tests/InstrumentedInputReader.cpp
+++ b/services/inputflinger/tests/InstrumentedInputReader.cpp
@@ -38,13 +38,13 @@
 }
 
 std::shared_ptr<InputDevice> InstrumentedInputReader::createDeviceLocked(
-        int32_t eventHubId, const InputDeviceIdentifier& identifier) REQUIRES(mLock) {
+        nsecs_t when, int32_t eventHubId, const InputDeviceIdentifier& identifier) REQUIRES(mLock) {
     if (!mNextDevices.empty()) {
         std::shared_ptr<InputDevice> device(std::move(mNextDevices.front()));
         mNextDevices.pop();
         return device;
     }
-    return InputReader::createDeviceLocked(eventHubId, identifier);
+    return InputReader::createDeviceLocked(when, eventHubId, identifier);
 }
 
 } // namespace android
diff --git a/services/inputflinger/tests/InstrumentedInputReader.h b/services/inputflinger/tests/InstrumentedInputReader.h
index fef58ec..ca85558 100644
--- a/services/inputflinger/tests/InstrumentedInputReader.h
+++ b/services/inputflinger/tests/InstrumentedInputReader.h
@@ -44,7 +44,7 @@
 
 protected:
     virtual std::shared_ptr<InputDevice> createDeviceLocked(
-            int32_t eventHubId, const InputDeviceIdentifier& identifier);
+            nsecs_t when, int32_t eventHubId, const InputDeviceIdentifier& identifier);
 
     class FakeInputReaderContext : public ContextImpl {
     public:
diff --git a/services/inputflinger/tests/PointerChoreographer_test.cpp b/services/inputflinger/tests/PointerChoreographer_test.cpp
new file mode 100644
index 0000000..7237424
--- /dev/null
+++ b/services/inputflinger/tests/PointerChoreographer_test.cpp
@@ -0,0 +1,89 @@
+/*
+ * 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 "../PointerChoreographer.h"
+
+#include <gtest/gtest.h>
+#include <vector>
+
+#include "TestInputListener.h"
+
+namespace android {
+
+// Helpers to std::visit with lambdas.
+template <typename... V>
+struct Visitor : V... {};
+template <typename... V>
+Visitor(V...) -> Visitor<V...>;
+
+// --- PointerChoreographerTest ---
+
+class PointerChoreographerTest : public testing::Test, public PointerChoreographerPolicyInterface {
+protected:
+    TestInputListener mTestListener;
+    PointerChoreographer mChoreographer{mTestListener, *this};
+
+    std::shared_ptr<PointerControllerInterface> createPointerController() { return {}; }
+};
+
+TEST_F(PointerChoreographerTest, ForwardsArgsToInnerListener) {
+    const std::vector<NotifyArgs> allArgs{NotifyInputDevicesChangedArgs{},
+                                          NotifyConfigurationChangedArgs{},
+                                          NotifyKeyArgs{},
+                                          NotifyMotionArgs{},
+                                          NotifySensorArgs{},
+                                          NotifySwitchArgs{},
+                                          NotifyDeviceResetArgs{},
+                                          NotifyPointerCaptureChangedArgs{},
+                                          NotifyVibratorStateArgs{}};
+
+    for (auto notifyArgs : allArgs) {
+        mChoreographer.notify(notifyArgs);
+        EXPECT_NO_FATAL_FAILURE(
+                std::visit(Visitor{
+                                   [&](const NotifyInputDevicesChangedArgs& args) {
+                                       mTestListener.assertNotifyInputDevicesChangedWasCalled();
+                                   },
+                                   [&](const NotifyConfigurationChangedArgs& args) {
+                                       mTestListener.assertNotifyConfigurationChangedWasCalled();
+                                   },
+                                   [&](const NotifyKeyArgs& args) {
+                                       mTestListener.assertNotifyKeyWasCalled();
+                                   },
+                                   [&](const NotifyMotionArgs& args) {
+                                       mTestListener.assertNotifyMotionWasCalled();
+                                   },
+                                   [&](const NotifySensorArgs& args) {
+                                       mTestListener.assertNotifySensorWasCalled();
+                                   },
+                                   [&](const NotifySwitchArgs& args) {
+                                       mTestListener.assertNotifySwitchWasCalled();
+                                   },
+                                   [&](const NotifyDeviceResetArgs& args) {
+                                       mTestListener.assertNotifyDeviceResetWasCalled();
+                                   },
+                                   [&](const NotifyPointerCaptureChangedArgs& args) {
+                                       mTestListener.assertNotifyCaptureWasCalled();
+                                   },
+                                   [&](const NotifyVibratorStateArgs& args) {
+                                       mTestListener.assertNotifyVibratorStateWasCalled();
+                                   },
+                           },
+                           notifyArgs));
+    }
+}
+
+} // namespace android
diff --git a/services/inputflinger/tests/UinputDevice.cpp b/services/inputflinger/tests/UinputDevice.cpp
index 7ceaccf..acc7023 100644
--- a/services/inputflinger/tests/UinputDevice.cpp
+++ b/services/inputflinger/tests/UinputDevice.cpp
@@ -157,6 +157,18 @@
     injectEvent(EV_SYN, SYN_REPORT, 0);
 }
 
+// --- UinputKeyboardWithHidUsage ---
+
+UinputKeyboardWithHidUsage::UinputKeyboardWithHidUsage(std::initializer_list<int> keys)
+      : UinputKeyboard(DEVICE_NAME, PRODUCT_ID, keys) {}
+
+void UinputKeyboardWithHidUsage::configureDevice(int fd, uinput_user_dev* device) {
+    UinputKeyboard::configureDevice(fd, device);
+
+    ioctl(fd, UI_SET_EVBIT, EV_MSC);
+    ioctl(fd, UI_SET_MSCBIT, MSC_SCAN);
+}
+
 // --- UinputTouchScreen ---
 
 UinputTouchScreen::UinputTouchScreen(const Rect& size, const std::string& physicalPort)
@@ -177,6 +189,7 @@
     ioctl(fd, UI_SET_ABSBIT, ABS_MT_POSITION_Y);
     ioctl(fd, UI_SET_ABSBIT, ABS_MT_TRACKING_ID);
     ioctl(fd, UI_SET_ABSBIT, ABS_MT_TOOL_TYPE);
+    ioctl(fd, UI_SET_ABSBIT, ABS_MT_PRESSURE);
     ioctl(fd, UI_SET_PROPBIT, INPUT_PROP_DIRECT);
     if (!mPhysicalPort.empty()) {
         ioctl(fd, UI_SET_PHYS, mPhysicalPort.c_str());
@@ -194,6 +207,8 @@
     device->absmax[ABS_MT_TRACKING_ID] = RAW_ID_MAX;
     device->absmin[ABS_MT_TOOL_TYPE] = MT_TOOL_FINGER;
     device->absmax[ABS_MT_TOOL_TYPE] = MT_TOOL_MAX;
+    device->absmin[ABS_MT_PRESSURE] = RAW_PRESSURE_MIN;
+    device->absmax[ABS_MT_PRESSURE] = RAW_PRESSURE_MAX;
 }
 
 void UinputTouchScreen::sendSlot(int32_t slot) {
@@ -206,6 +221,7 @@
 
 void UinputTouchScreen::sendDown(const Point& point) {
     injectEvent(EV_KEY, BTN_TOUCH, 1);
+    injectEvent(EV_ABS, ABS_MT_PRESSURE, RAW_PRESSURE_MAX);
     injectEvent(EV_ABS, ABS_MT_POSITION_X, point.x);
     injectEvent(EV_ABS, ABS_MT_POSITION_Y, point.y);
 }
@@ -215,6 +231,10 @@
     injectEvent(EV_ABS, ABS_MT_POSITION_Y, point.y);
 }
 
+void UinputTouchScreen::sendPressure(int32_t pressure) {
+    injectEvent(EV_ABS, ABS_MT_PRESSURE, pressure);
+}
+
 void UinputTouchScreen::sendPointerUp() {
     sendTrackingId(0xffffffff);
 }
diff --git a/services/inputflinger/tests/UinputDevice.h b/services/inputflinger/tests/UinputDevice.h
index 5b07465..d4b4e77 100644
--- a/services/inputflinger/tests/UinputDevice.h
+++ b/services/inputflinger/tests/UinputDevice.h
@@ -165,13 +165,30 @@
     explicit UinputExternalStylusWithPressure();
 };
 
+// --- UinputKeyboardWithUsage ---
+// A keyboard that supports EV_MSC MSC_SCAN through which it can report HID usage codes.
+
+class UinputKeyboardWithHidUsage : public UinputKeyboard {
+public:
+    static constexpr const char* DEVICE_NAME = "Test Uinput Keyboard With Usage";
+    static constexpr int16_t PRODUCT_ID = 47;
+
+    template <class D, class... Ts>
+    friend std::unique_ptr<D> createUinputDevice(Ts... args);
+
+protected:
+    explicit UinputKeyboardWithHidUsage(std::initializer_list<int> keys);
+
+    void configureDevice(int fd, uinput_user_dev* device) override;
+};
+
 // --- UinputTouchScreen ---
 
 // A multi-touch touchscreen device with specific size that also supports styluses.
 class UinputTouchScreen : public UinputKeyboard {
 public:
     static constexpr const char* DEVICE_NAME = "Test Uinput Touch Screen";
-    static constexpr int16_t PRODUCT_ID = 47;
+    static constexpr int16_t PRODUCT_ID = 48;
 
     static const int32_t RAW_TOUCH_MIN = 0;
     static const int32_t RAW_TOUCH_MAX = 31;
@@ -189,6 +206,7 @@
     void sendTrackingId(int32_t trackingId);
     void sendDown(const Point& point);
     void sendMove(const Point& point);
+    void sendPressure(int32_t pressure);
     void sendPointerUp();
     void sendUp();
     void sendToolType(int32_t toolType);
diff --git a/services/sensorservice/RecentEventLogger.cpp b/services/sensorservice/RecentEventLogger.cpp
index d7ca6e1..47fa8b3 100644
--- a/services/sensorservice/RecentEventLogger.cpp
+++ b/services/sensorservice/RecentEventLogger.cpp
@@ -83,7 +83,7 @@
         }
         buffer.append("\n");
     }
-    return std::string(buffer.string());
+    return std::string(buffer.c_str());
 }
 
 /**
diff --git a/services/sensorservice/SensorDevice.cpp b/services/sensorservice/SensorDevice.cpp
index 10ca990..3155b4c 100644
--- a/services/sensorservice/SensorDevice.cpp
+++ b/services/sensorservice/SensorDevice.cpp
@@ -298,7 +298,7 @@
         result.appendFormat("}, selected = %.2f ms\n", info.bestBatchParams.mTBatch / 1e6f);
     }
 
-    return result.string();
+    return result.c_str();
 }
 
 /**
diff --git a/services/sensorservice/SensorDirectConnection.cpp b/services/sensorservice/SensorDirectConnection.cpp
index 4fff8bb..555b80a 100644
--- a/services/sensorservice/SensorDirectConnection.cpp
+++ b/services/sensorservice/SensorDirectConnection.cpp
@@ -63,7 +63,7 @@
 void SensorService::SensorDirectConnection::dump(String8& result) const {
     Mutex::Autolock _l(mConnectionLock);
     result.appendFormat("\tPackage %s, HAL channel handle %d, total sensor activated %zu\n",
-            String8(mOpPackageName).string(), getHalChannelHandle(), mActivated.size());
+            String8(mOpPackageName).c_str(), getHalChannelHandle(), mActivated.size());
     for (auto &i : mActivated) {
         result.appendFormat("\t\tSensor %#08x, rate %d\n", i.first, i.second);
     }
@@ -79,7 +79,7 @@
 void SensorService::SensorDirectConnection::dump(ProtoOutputStream* proto) const {
     using namespace service::SensorDirectConnectionProto;
     Mutex::Autolock _l(mConnectionLock);
-    proto->write(PACKAGE_NAME, std::string(String8(mOpPackageName).string()));
+    proto->write(PACKAGE_NAME, std::string(String8(mOpPackageName).c_str()));
     proto->write(HAL_CHANNEL_HANDLE, getHalChannelHandle());
     proto->write(NUM_SENSOR_ACTIVATED, int(mActivated.size()));
     for (auto &i : mActivated) {
diff --git a/services/sensorservice/SensorEventConnection.cpp b/services/sensorservice/SensorEventConnection.cpp
index dc5070c..dc86577 100644
--- a/services/sensorservice/SensorEventConnection.cpp
+++ b/services/sensorservice/SensorEventConnection.cpp
@@ -90,12 +90,12 @@
         result.append("NORMAL\n");
     }
     result.appendFormat("\t %s | WakeLockRefCount %d | uid %d | cache size %d | "
-            "max cache size %d\n", mPackageName.string(), mWakeLockRefCount, mUid, mCacheSize,
+            "max cache size %d\n", mPackageName.c_str(), mWakeLockRefCount, mUid, mCacheSize,
             mMaxCacheSize);
     for (auto& it : mSensorInfo) {
         const FlushInfo& flushInfo = it.second;
         result.appendFormat("\t %s 0x%08x | status: %s | pending flush events %d \n",
-                            mService->getSensorName(it.first).string(),
+                            mService->getSensorName(it.first).c_str(),
                             it.first,
                             flushInfo.mFirstFlushPending ? "First flush pending" :
                                                            "active",
@@ -131,7 +131,7 @@
     } else {
         proto->write(OPERATING_MODE, OP_MODE_NORMAL);
     }
-    proto->write(PACKAGE_NAME, std::string(mPackageName.string()));
+    proto->write(PACKAGE_NAME, std::string(mPackageName.c_str()));
     proto->write(WAKE_LOCK_REF_COUNT, int32_t(mWakeLockRefCount));
     proto->write(UID, int32_t(mUid));
     proto->write(CACHE_SIZE, int32_t(mCacheSize));
@@ -848,13 +848,13 @@
             if (numBytesRead == sizeof(sensors_event_t)) {
                 if (!mDataInjectionMode) {
                     ALOGE("Data injected in normal mode, dropping event"
-                          "package=%s uid=%d", mPackageName.string(), mUid);
+                          "package=%s uid=%d", mPackageName.c_str(), mUid);
                     // Unregister call backs.
                     return 0;
                 }
                 if (!mService->isAllowListedPackage(mPackageName)) {
                     ALOGE("App not allowed to inject data, dropping event"
-                          "package=%s uid=%d", mPackageName.string(), mUid);
+                          "package=%s uid=%d", mPackageName.c_str(), mUid);
                     return 0;
                 }
                 sensors_event_t sensor_event;
diff --git a/services/sensorservice/SensorList.cpp b/services/sensorservice/SensorList.cpp
index daff4d0..7e30206 100644
--- a/services/sensorservice/SensorList.cpp
+++ b/services/sensorservice/SensorList.cpp
@@ -150,12 +150,12 @@
                     "%#010x) %-25s | %-15s | ver: %" PRId32 " | type: %20s(%" PRId32
                         ") | perm: %s | flags: 0x%08x\n",
                     s.getHandle(),
-                    s.getName().string(),
-                    s.getVendor().string(),
+                    s.getName().c_str(),
+                    s.getVendor().c_str(),
                     s.getVersion(),
-                    s.getStringType().string(),
+                    s.getStringType().c_str(),
                     s.getType(),
-                    s.getRequiredPermission().size() ? s.getRequiredPermission().string() : "n/a",
+                    s.getRequiredPermission().size() ? s.getRequiredPermission().c_str() : "n/a",
                     static_cast<int>(s.getFlags()));
 
             result.append("\t");
@@ -224,7 +224,7 @@
             }
             return true;
         });
-    return std::string(result.string());
+    return std::string(result.c_str());
 }
 
 /**
@@ -241,13 +241,13 @@
     forEachSensor([&proto] (const Sensor& s) -> bool {
         const uint64_t token = proto->start(SENSORS);
         proto->write(HANDLE, s.getHandle());
-        proto->write(NAME, std::string(s.getName().string()));
-        proto->write(VENDOR, std::string(s.getVendor().string()));
+        proto->write(NAME, std::string(s.getName().c_str()));
+        proto->write(VENDOR, std::string(s.getVendor().c_str()));
         proto->write(VERSION, s.getVersion());
-        proto->write(STRING_TYPE, std::string(s.getStringType().string()));
+        proto->write(STRING_TYPE, std::string(s.getStringType().c_str()));
         proto->write(TYPE, s.getType());
         proto->write(REQUIRED_PERMISSION, std::string(s.getRequiredPermission().size() ?
-                s.getRequiredPermission().string() : ""));
+                s.getRequiredPermission().c_str() : ""));
         proto->write(FLAGS, int(s.getFlags()));
         switch (s.getReportingMode()) {
             case AREPORTING_MODE_CONTINUOUS:
diff --git a/services/sensorservice/SensorRegistrationInfo.h b/services/sensorservice/SensorRegistrationInfo.h
index a34a65b..dc9e821 100644
--- a/services/sensorservice/SensorRegistrationInfo.h
+++ b/services/sensorservice/SensorRegistrationInfo.h
@@ -93,7 +93,7 @@
         using namespace service::SensorRegistrationInfoProto;
         proto->write(TIMESTAMP_SEC, int64_t(mRealtimeSec));
         proto->write(SENSOR_HANDLE, mSensorHandle);
-        proto->write(PACKAGE_NAME, std::string(mPackageName.string()));
+        proto->write(PACKAGE_NAME, std::string(mPackageName.c_str()));
         proto->write(PID, int32_t(mPid));
         proto->write(UID, int32_t(mUid));
         proto->write(SAMPLING_RATE_US, mSamplingRateUs);
diff --git a/services/sensorservice/SensorService.cpp b/services/sensorservice/SensorService.cpp
index cfafc69..9c51fd9 100644
--- a/services/sensorservice/SensorService.cpp
+++ b/services/sensorservice/SensorService.cpp
@@ -584,7 +584,7 @@
         }
         if (args.size() > 0) {
             Mode targetOperatingMode = NORMAL;
-            std::string inputStringMode = String8(args[0]).string();
+            std::string inputStringMode = String8(args[0]).c_str();
             if (getTargetOperatingMode(inputStringMode, &targetOperatingMode)) {
               status_t error = changeOperatingMode(args, targetOperatingMode);
               // Dump the latest state only if no error was encountered.
@@ -623,13 +623,13 @@
             for (auto&& i : mRecentEvent) {
                 std::shared_ptr<SensorInterface> s = getSensorInterfaceFromHandle(i.first);
                 if (!i.second->isEmpty() && s != nullptr) {
-                    if (privileged || s->getSensor().getRequiredPermission().isEmpty()) {
+                    if (privileged || s->getSensor().getRequiredPermission().empty()) {
                         i.second->setFormat("normal");
                     } else {
                         i.second->setFormat("mask_data");
                     }
                     // if there is events and sensor does not need special permission.
-                    result.appendFormat("%s: ", s->getSensor().getName().string());
+                    result.appendFormat("%s: ", s->getSensor().getName().c_str());
                     result.append(i.second->dump().c_str());
                 }
             }
@@ -640,7 +640,7 @@
                 int handle = mActiveSensors.keyAt(i);
                 if (dev.isSensorActive(handle)) {
                     result.appendFormat("%s (handle=0x%08x, connections=%zu)\n",
-                            getSensorName(handle).string(),
+                            getSensorName(handle).c_str(),
                             handle,
                             mActiveSensors.valueAt(i)->getNumConnections());
                 }
@@ -656,14 +656,14 @@
                    result.appendFormat(" NORMAL\n");
                    break;
                case RESTRICTED:
-                   result.appendFormat(" RESTRICTED : %s\n", mAllowListedPackage.string());
+                   result.appendFormat(" RESTRICTED : %s\n", mAllowListedPackage.c_str());
                    break;
                case DATA_INJECTION:
-                   result.appendFormat(" DATA_INJECTION : %s\n", mAllowListedPackage.string());
+                   result.appendFormat(" DATA_INJECTION : %s\n", mAllowListedPackage.c_str());
                    break;
                case REPLAY_DATA_INJECTION:
                    result.appendFormat(" REPLAY_DATA_INJECTION : %s\n",
-                            mAllowListedPackage.string());
+                            mAllowListedPackage.c_str());
                    break;
                default:
                    result.appendFormat(" UNKNOWN\n");
@@ -705,7 +705,7 @@
             } while(startIndex != currentIndex);
         }
     }
-    write(fd, result.string(), result.size());
+    write(fd, result.c_str(), result.size());
     return NO_ERROR;
 }
 
@@ -749,11 +749,11 @@
     for (auto&& i : mRecentEvent) {
         std::shared_ptr<SensorInterface> s = getSensorInterfaceFromHandle(i.first);
         if (!i.second->isEmpty() && s != nullptr) {
-            i.second->setFormat(privileged || s->getSensor().getRequiredPermission().isEmpty() ?
+            i.second->setFormat(privileged || s->getSensor().getRequiredPermission().empty() ?
                     "normal" : "mask_data");
             const uint64_t mToken = proto.start(service::SensorEventsProto::RECENT_EVENTS_LOGS);
             proto.write(service::SensorEventsProto::RecentEventsLog::NAME,
-                    std::string(s->getSensor().getName().string()));
+                    std::string(s->getSensor().getName().c_str()));
             i.second->dump(&proto);
             proto.end(mToken);
         }
@@ -767,7 +767,7 @@
         if (dev.isSensorActive(handle)) {
             token = proto.start(ACTIVE_SENSORS);
             proto.write(service::ActiveSensorProto::NAME,
-                    std::string(getSensorName(handle).string()));
+                    std::string(getSensorName(handle).c_str()));
             proto.write(service::ActiveSensorProto::HANDLE, handle);
             proto.write(service::ActiveSensorProto::NUM_CONNECTIONS,
                     int(mActiveSensors.valueAt(i)->getNumConnections()));
@@ -785,11 +785,11 @@
             break;
         case RESTRICTED:
             proto.write(OPERATING_MODE, OP_MODE_RESTRICTED);
-            proto.write(WHITELISTED_PACKAGE, std::string(mAllowListedPackage.string()));
+            proto.write(WHITELISTED_PACKAGE, std::string(mAllowListedPackage.c_str()));
             break;
         case DATA_INJECTION:
             proto.write(OPERATING_MODE, OP_MODE_DATA_INJECTION);
-            proto.write(WHITELISTED_PACKAGE, std::string(mAllowListedPackage.string()));
+            proto.write(WHITELISTED_PACKAGE, std::string(mAllowListedPackage.c_str()));
             break;
         default:
             proto.write(OPERATING_MODE, OP_MODE_UNKNOWN);
@@ -932,8 +932,8 @@
     PermissionController pc;
     uid = pc.getPackageUid(packageName, 0);
     if (uid <= 0) {
-        ALOGE("Unknown package: '%s'", String8(packageName).string());
-        dprintf(err, "Unknown package: '%s'\n", String8(packageName).string());
+        ALOGE("Unknown package: '%s'", String8(packageName).c_str());
+        dprintf(err, "Unknown package: '%s'\n", String8(packageName).c_str());
         return BAD_VALUE;
     }
 
@@ -958,7 +958,7 @@
     if (args[2] == String16("active")) {
         active = true;
     } else if ((args[2] != String16("idle"))) {
-        ALOGE("Expected active or idle but got: '%s'", String8(args[2]).string());
+        ALOGE("Expected active or idle but got: '%s'", String8(args[2]).c_str());
         return BAD_VALUE;
     }
 
@@ -1495,7 +1495,7 @@
         accessibleSensorList.add(sensor);
     } else if (sensor.getType() != SENSOR_TYPE_HEAD_TRACKER) {
         ALOGI("Skipped sensor %s because it requires permission %s and app op %" PRId32,
-        sensor.getName().string(), sensor.getRequiredPermission().string(),
+        sensor.getName().c_str(), sensor.getRequiredPermission().c_str(),
         sensor.getRequiredAppOp());
     }
 }
@@ -2217,10 +2217,10 @@
             !isAudioServerOrSystemServerUid(IPCThreadState::self()->getCallingUid())) {
         if (!mHtRestricted) {
             ALOGI("Permitting access to HT sensor type outside system (%s)",
-                  String8(opPackageName).string());
+                  String8(opPackageName).c_str());
         } else {
-            ALOGW("%s %s a sensor (%s) as a non-system client", String8(opPackageName).string(),
-                  operation, sensor.getName().string());
+            ALOGW("%s %s a sensor (%s) as a non-system client", String8(opPackageName).c_str(),
+                  operation, sensor.getName().c_str());
             return false;
         }
     }
@@ -2253,8 +2253,8 @@
     }
 
     if (!canAccess) {
-        ALOGE("%s %s a sensor (%s) without holding %s", String8(opPackageName).string(),
-              operation, sensor.getName().string(), sensor.getRequiredPermission().string());
+        ALOGE("%s %s a sensor (%s) without holding %s", String8(opPackageName).c_str(),
+              operation, sensor.getName().c_str(), sensor.getRequiredPermission().c_str());
     }
 
     return canAccess;
@@ -2371,7 +2371,7 @@
         mCurrentOperatingMode = RESTRICTED;
         // temporarily stop all sensor direct report and disable sensors
         disableAllSensorsLocked(&connLock);
-        mAllowListedPackage.setTo(String8(args[1]));
+        mAllowListedPackage = String8(args[1]);
         return status_t(NO_ERROR);
       case REPLAY_DATA_INJECTION:
         if (SensorServiceUtil::isUserBuild()) {
@@ -2391,7 +2391,7 @@
                 // Re-enable sensors.
                 dev.enableAllSensors();
             }
-            mAllowListedPackage.setTo(String8(args[1]));
+            mAllowListedPackage = String8(args[1]);
             return NO_ERROR;
         } else {
             // Transition to data injection mode supported only from NORMAL mode.
@@ -2434,7 +2434,7 @@
 }
 
 bool SensorService::isAllowListedPackage(const String8& packageName) {
-    return (packageName.contains(mAllowListedPackage.string()));
+    return (packageName.contains(mAllowListedPackage.c_str()));
 }
 
 bool SensorService::isOperationRestrictedLocked(const String16& opPackageName) {
diff --git a/services/sensorservice/hidl/utils.cpp b/services/sensorservice/hidl/utils.cpp
index 5fa594d..d338d02 100644
--- a/services/sensorservice/hidl/utils.cpp
+++ b/services/sensorservice/hidl/utils.cpp
@@ -32,8 +32,8 @@
     SensorInfo dst;
     const String8& name = src.getName();
     const String8& vendor = src.getVendor();
-    dst.name = hidl_string{name.string(), name.size()};
-    dst.vendor = hidl_string{vendor.string(), vendor.size()};
+    dst.name = hidl_string{name.c_str(), name.size()};
+    dst.vendor = hidl_string{vendor.c_str(), vendor.size()};
     dst.version = src.getVersion();
     dst.sensorHandle = src.getHandle();
     dst.type = static_cast<::android::hardware::sensors::V1_0::SensorType>(
diff --git a/services/sensorservice/tests/sensorservicetest.cpp b/services/sensorservice/tests/sensorservicetest.cpp
index 1baf397..92956d6 100644
--- a/services/sensorservice/tests/sensorservicetest.cpp
+++ b/services/sensorservice/tests/sensorservicetest.cpp
@@ -116,7 +116,7 @@
 
     Sensor const* accelerometer = mgr.getDefaultSensor(Sensor::TYPE_ACCELEROMETER);
     printf("accelerometer=%p (%s)\n",
-            accelerometer, accelerometer->getName().string());
+            accelerometer, accelerometer->getName().c_str());
 
     sStartTime = systemTime();
 
diff --git a/services/surfaceflinger/Android.bp b/services/surfaceflinger/Android.bp
index 0101c17..eda52bf 100644
--- a/services/surfaceflinger/Android.bp
+++ b/services/surfaceflinger/Android.bp
@@ -187,6 +187,7 @@
         "Scheduler/MessageQueue.cpp",
         "Scheduler/RefreshRateSelector.cpp",
         "Scheduler/Scheduler.cpp",
+        "Scheduler/SmallAreaDetectionAllowMappings.cpp",
         "Scheduler/VSyncDispatchTimerQueue.cpp",
         "Scheduler/VSyncPredictor.cpp",
         "Scheduler/VSyncReactor.cpp",
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/TexturePool.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/TexturePool.h
index 9f6141a..d607c75 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/TexturePool.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/impl/planner/TexturePool.h
@@ -66,7 +66,7 @@
     TexturePool(renderengine::RenderEngine& renderEngine)
           : mRenderEngine(renderEngine), mEnabled(false) {}
 
-    virtual ~TexturePool();
+    virtual ~TexturePool() = default;
 
     // Sets the display size for the texture pool.
     // This will trigger a reallocation for all remaining textures in the pool.
@@ -83,10 +83,11 @@
     // be held by the pool. This is useful when the active display changes.
     void setEnabled(bool enable);
 
-    void dump(std::string& out) const EXCLUDES(mMutex);
+    void dump(std::string& out) const;
 
 protected:
     // Proteted visibility so that they can be used for testing
+    const static constexpr size_t kMinPoolSize = 3;
     const static constexpr size_t kMaxPoolSize = 4;
 
     struct Entry {
@@ -95,20 +96,16 @@
     };
 
     std::deque<Entry> mPool;
-    std::future<std::shared_ptr<renderengine::ExternalTexture>> mGenTextureFuture;
 
 private:
-    std::shared_ptr<renderengine::ExternalTexture> genTexture(ui::Size size);
+    std::shared_ptr<renderengine::ExternalTexture> genTexture();
     // Returns a previously borrowed texture to the pool.
     void returnTexture(std::shared_ptr<renderengine::ExternalTexture>&& texture,
                        const sp<Fence>& fence);
-    void genTextureAsyncIfNeeded() REQUIRES(mMutex);
-    void resetPool() REQUIRES(mMutex);
-    renderengine::RenderEngine& mRenderEngine GUARDED_BY(mRenderEngineMutex);
-    ui::Size mSize GUARDED_BY(mMutex);
+    void allocatePool();
+    renderengine::RenderEngine& mRenderEngine;
+    ui::Size mSize;
     bool mEnabled;
-    mutable std::mutex mMutex;
-    mutable std::mutex mRenderEngineMutex;
 };
 
 } // namespace android::compositionengine::impl::planner
diff --git a/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp b/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp
index b492b6a..22db247 100644
--- a/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp
@@ -13,7 +13,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 #include <DisplayHardware/Hal.h>
 #include <android-base/stringprintf.h>
 #include <compositionengine/DisplayColorProfile.h>
@@ -26,7 +25,7 @@
 #include <cstdint>
 #include "system/graphics-base-v1.0.h"
 
-#include <ui/DataspaceUtils.h>
+#include <ui/HdrRenderTypeUtils.h>
 
 // TODO(b/129481165): remove the #pragma below and fix conversion issues
 #pragma clang diagnostic push
@@ -312,12 +311,19 @@
         }
     }
 
+    auto pixelFormat = layerFEState->buffer ? std::make_optional(static_cast<ui::PixelFormat>(
+                                                      layerFEState->buffer->getPixelFormat()))
+                                            : std::nullopt;
+
+    auto hdrRenderType =
+            getHdrRenderType(outputState.dataspace, pixelFormat, layerFEState->desiredHdrSdrRatio);
+
     // Determine the output dependent dataspace for this layer. If it is
     // colorspace agnostic, it just uses the dataspace chosen for the output to
     // avoid the need for color conversion.
     // For now, also respect the colorspace agnostic flag if we're drawing to HDR, to avoid drastic
     // luminance shift. TODO(b/292162273): we should check if that's true though.
-    state.dataspace = layerFEState->isColorspaceAgnostic && !isHdrDataspace(outputState.dataspace)
+    state.dataspace = layerFEState->isColorspaceAgnostic && hdrRenderType == HdrRenderType::SDR
             ? outputState.dataspace
             : layerFEState->dataspace;
 
@@ -332,10 +338,14 @@
                 (state.dataspace & HAL_DATASPACE_RANGE_MASK) | HAL_DATASPACE_TRANSFER_SRGB);
     }
 
+    // re-get HdrRenderType after the dataspace gets changed.
+    hdrRenderType =
+            getHdrRenderType(state.dataspace, pixelFormat, layerFEState->desiredHdrSdrRatio);
+
     // For hdr content, treat the white point as the display brightness - HDR content should not be
     // boosted or dimmed.
     // If the layer explicitly requests to disable dimming, then don't dim either.
-    if (isHdrDataspace(state.dataspace) ||
+    if (hdrRenderType == HdrRenderType::GENERIC_HDR ||
         getOutput().getState().displayBrightnessNits == getOutput().getState().sdrWhitePointNits ||
         getOutput().getState().displayBrightnessNits == 0.f || !layerFEState->dimmingEnabled) {
         state.dimmingRatio = 1.f;
@@ -344,8 +354,7 @@
         float layerBrightnessNits = getOutput().getState().sdrWhitePointNits;
         // RANGE_EXTENDED can "self-promote" to HDR, but is still rendered for a particular
         // range that we may need to re-adjust to the current display conditions
-        if ((state.dataspace & HAL_DATASPACE_RANGE_MASK) == HAL_DATASPACE_RANGE_EXTENDED &&
-            layerFEState->currentHdrSdrRatio > 1.01f) {
+        if (hdrRenderType == HdrRenderType::DISPLAY_HDR) {
             layerBrightnessNits *= layerFEState->currentHdrSdrRatio;
         }
         state.dimmingRatio =
diff --git a/services/surfaceflinger/CompositionEngine/src/planner/LayerState.cpp b/services/surfaceflinger/CompositionEngine/src/planner/LayerState.cpp
index f439caf..8dab6ce 100644
--- a/services/surfaceflinger/CompositionEngine/src/planner/LayerState.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/planner/LayerState.cpp
@@ -34,7 +34,7 @@
                          [](const mat4& mat) {
                              using namespace std::string_literals;
                              std::vector<std::string> split =
-                                     base::Split(std::string(mat.asString().string()), "\n"s);
+                                     base::Split(std::string(mat.asString().c_str()), "\n"s);
                              split.pop_back(); // Strip the last (empty) line
                              return split;
                          }}) {
diff --git a/services/surfaceflinger/CompositionEngine/src/planner/Planner.cpp b/services/surfaceflinger/CompositionEngine/src/planner/Planner.cpp
index 54133d9..5e6cade 100644
--- a/services/surfaceflinger/CompositionEngine/src/planner/Planner.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/planner/Planner.cpp
@@ -216,32 +216,32 @@
                 base::StringAppendF(&result,
                                     "Expected two layer stack hashes, e.g. '--planner %s "
                                     "<left_hash> <right_hash>'\n",
-                                    command.string());
+                                    command.c_str());
                 return;
             }
             if (args.size() > 4) {
                 base::StringAppendF(&result,
                                     "Too many arguments found, expected '--planner %s <left_hash> "
                                     "<right_hash>'\n",
-                                    command.string());
+                                    command.c_str());
                 return;
             }
 
             const String8 leftHashString(args[2]);
             size_t leftHash = 0;
-            int fieldsRead = sscanf(leftHashString.string(), "%zx", &leftHash);
+            int fieldsRead = sscanf(leftHashString.c_str(), "%zx", &leftHash);
             if (fieldsRead != 1) {
                 base::StringAppendF(&result, "Failed to parse %s as a size_t\n",
-                                    leftHashString.string());
+                                    leftHashString.c_str());
                 return;
             }
 
             const String8 rightHashString(args[3]);
             size_t rightHash = 0;
-            fieldsRead = sscanf(rightHashString.string(), "%zx", &rightHash);
+            fieldsRead = sscanf(rightHashString.c_str(), "%zx", &rightHash);
             if (fieldsRead != 1) {
                 base::StringAppendF(&result, "Failed to parse %s as a size_t\n",
-                                    rightHashString.string());
+                                    rightHashString.c_str());
                 return;
             }
 
@@ -252,22 +252,22 @@
             if (args.size() < 3) {
                 base::StringAppendF(&result,
                                     "Expected a layer stack hash, e.g. '--planner %s <hash>'\n",
-                                    command.string());
+                                    command.c_str());
                 return;
             }
             if (args.size() > 3) {
                 base::StringAppendF(&result,
                                     "Too many arguments found, expected '--planner %s <hash>'\n",
-                                    command.string());
+                                    command.c_str());
                 return;
             }
 
             const String8 hashString(args[2]);
             size_t hash = 0;
-            const int fieldsRead = sscanf(hashString.string(), "%zx", &hash);
+            const int fieldsRead = sscanf(hashString.c_str(), "%zx", &hash);
             if (fieldsRead != 1) {
                 base::StringAppendF(&result, "Failed to parse %s as a size_t\n",
-                                    hashString.string());
+                                    hashString.c_str());
                 return;
             }
 
@@ -279,20 +279,20 @@
         } else if (command == "--similar" || command == "-s") {
             if (args.size() < 3) {
                 base::StringAppendF(&result, "Expected a plan string, e.g. '--planner %s <plan>'\n",
-                                    command.string());
+                                    command.c_str());
                 return;
             }
             if (args.size() > 3) {
                 base::StringAppendF(&result,
                                     "Too many arguments found, expected '--planner %s <plan>'\n",
-                                    command.string());
+                                    command.c_str());
                 return;
             }
 
             const String8 planString(args[2]);
-            std::optional<Plan> plan = Plan::fromString(std::string(planString.string()));
+            std::optional<Plan> plan = Plan::fromString(std::string(planString.c_str()));
             if (!plan) {
-                base::StringAppendF(&result, "Failed to parse %s as a Plan\n", planString.string());
+                base::StringAppendF(&result, "Failed to parse %s as a Plan\n", planString.c_str());
                 return;
             }
 
@@ -302,7 +302,7 @@
         } else if (command == "--layers" || command == "-l") {
             mFlattener.dumpLayers(result);
         } else {
-            base::StringAppendF(&result, "Unknown command '%s'\n\n", command.string());
+            base::StringAppendF(&result, "Unknown command '%s'\n\n", command.c_str());
             dumpUsage(result);
         }
         return;
diff --git a/services/surfaceflinger/CompositionEngine/src/planner/TexturePool.cpp b/services/surfaceflinger/CompositionEngine/src/planner/TexturePool.cpp
index 10f58ce..54ecb56 100644
--- a/services/surfaceflinger/CompositionEngine/src/planner/TexturePool.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/planner/TexturePool.cpp
@@ -25,61 +25,31 @@
 
 namespace android::compositionengine::impl::planner {
 
-TexturePool::~TexturePool() {
-    if (mGenTextureFuture.valid()) {
-        mGenTextureFuture.get();
-    }
-}
-
-void TexturePool::resetPool() {
-    if (mGenTextureFuture.valid()) {
-        mGenTextureFuture.get();
-    }
+void TexturePool::allocatePool() {
     mPool.clear();
-    genTextureAsyncIfNeeded();
-}
-
-// Generate a new texture asynchronously so it will not require allocation on the main
-// thread.
-void TexturePool::genTextureAsyncIfNeeded() {
-    if (mEnabled && mSize.isValid() && !mGenTextureFuture.valid()) {
-        mGenTextureFuture = std::async(
-                std::launch::async, [&](ui::Size size) { return genTexture(size); }, mSize);
+    if (mEnabled && mSize.isValid()) {
+        mPool.resize(kMinPoolSize);
+        std::generate_n(mPool.begin(), kMinPoolSize, [&]() {
+            return Entry{genTexture(), nullptr};
+        });
     }
 }
 
 void TexturePool::setDisplaySize(ui::Size size) {
-    std::lock_guard lock(mMutex);
     if (mSize == size) {
         return;
     }
     mSize = size;
-    resetPool();
+    allocatePool();
 }
 
 std::shared_ptr<TexturePool::AutoTexture> TexturePool::borrowTexture() {
     if (mPool.empty()) {
-        std::lock_guard lock(mMutex);
-        std::shared_ptr<TexturePool::AutoTexture> tex;
-        if (mGenTextureFuture.valid()) {
-            tex = std::make_shared<AutoTexture>(*this, mGenTextureFuture.get(), nullptr);
-        } else {
-            tex = std::make_shared<AutoTexture>(*this, genTexture(mSize), nullptr);
-        }
-        // Speculatively generate a new texture, so that the next call does not need
-        // to wait for allocation.
-        genTextureAsyncIfNeeded();
-        return tex;
+        return std::make_shared<AutoTexture>(*this, genTexture(), nullptr);
     }
 
     const auto entry = mPool.front();
     mPool.pop_front();
-    if (mPool.empty()) {
-        std::lock_guard lock(mMutex);
-        // Similiarly generate a new texture when lending out the last entry, so that
-        // the next call does not need to wait for allocation.
-        genTextureAsyncIfNeeded();
-    }
     return std::make_shared<AutoTexture>(*this, entry.texture, entry.fence);
 }
 
@@ -90,8 +60,6 @@
         return;
     }
 
-    std::lock_guard lock(mMutex);
-
     // Or the texture on the floor if the pool is no longer tracking textures of the same size.
     if (static_cast<int32_t>(texture->getBuffer()->getWidth()) != mSize.getWidth() ||
         static_cast<int32_t>(texture->getBuffer()->getHeight()) != mSize.getHeight()) {
@@ -112,14 +80,13 @@
     mPool.push_back({std::move(texture), fence});
 }
 
-std::shared_ptr<renderengine::ExternalTexture> TexturePool::genTexture(ui::Size size) {
-    std::lock_guard lock(mRenderEngineMutex);
-    LOG_ALWAYS_FATAL_IF(!size.isValid(), "Attempted to generate texture with invalid size");
+std::shared_ptr<renderengine::ExternalTexture> TexturePool::genTexture() {
+    LOG_ALWAYS_FATAL_IF(!mSize.isValid(), "Attempted to generate texture with invalid size");
     return std::make_shared<
             renderengine::impl::
                     ExternalTexture>(sp<GraphicBuffer>::
-                                             make(static_cast<uint32_t>(size.getWidth()),
-                                                  static_cast<uint32_t>(size.getHeight()),
+                                             make(static_cast<uint32_t>(mSize.getWidth()),
+                                                  static_cast<uint32_t>(mSize.getHeight()),
                                                   HAL_PIXEL_FORMAT_RGBA_8888, 1U,
                                                   static_cast<uint64_t>(
                                                           GraphicBuffer::USAGE_HW_RENDER |
@@ -133,16 +100,13 @@
 
 void TexturePool::setEnabled(bool enabled) {
     mEnabled = enabled;
-
-    std::lock_guard lock(mMutex);
-    resetPool();
+    allocatePool();
 }
 
 void TexturePool::dump(std::string& out) const {
-    std::lock_guard lock(mMutex);
     base::StringAppendF(&out,
                         "TexturePool (%s) has %zu buffers of size [%" PRId32 ", %" PRId32 "]\n",
                         mEnabled ? "enabled" : "disabled", mPool.size(), mSize.width, mSize.height);
 }
 
-} // namespace android::compositionengine::impl::planner
+} // namespace android::compositionengine::impl::planner
\ No newline at end of file
diff --git a/services/surfaceflinger/CompositionEngine/tests/planner/TexturePoolTest.cpp b/services/surfaceflinger/CompositionEngine/tests/planner/TexturePoolTest.cpp
index 494a9f4..6fc90fe 100644
--- a/services/surfaceflinger/CompositionEngine/tests/planner/TexturePoolTest.cpp
+++ b/services/surfaceflinger/CompositionEngine/tests/planner/TexturePoolTest.cpp
@@ -32,9 +32,9 @@
 public:
     TestableTexturePool(renderengine::RenderEngine& renderEngine) : TexturePool(renderEngine) {}
 
+    size_t getMinPoolSize() const { return kMinPoolSize; }
     size_t getMaxPoolSize() const { return kMaxPoolSize; }
     size_t getPoolSize() const { return mPool.size(); }
-    size_t isGenTextureFutureValid() const { return mGenTextureFuture.valid(); }
 };
 
 struct TexturePoolTest : public testing::Test {
@@ -56,8 +56,16 @@
     TestableTexturePool mTexturePool = TestableTexturePool(mRenderEngine);
 };
 
-TEST_F(TexturePoolTest, preallocatesZeroSizePool) {
-    EXPECT_EQ(mTexturePool.getPoolSize(), 0u);
+TEST_F(TexturePoolTest, preallocatesMinPool) {
+    EXPECT_EQ(mTexturePool.getMinPoolSize(), mTexturePool.getPoolSize());
+}
+
+TEST_F(TexturePoolTest, doesNotAllocateBeyondMinPool) {
+    for (size_t i = 0; i < mTexturePool.getMinPoolSize() + 1; i++) {
+        auto texture = mTexturePool.borrowTexture();
+    }
+
+    EXPECT_EQ(mTexturePool.getMinPoolSize(), mTexturePool.getPoolSize());
 }
 
 TEST_F(TexturePoolTest, cyclesUpToMaxPoolSize) {
@@ -111,10 +119,10 @@
               static_cast<int32_t>(texture->get()->getBuffer()->getHeight()));
     mTexturePool.setDisplaySize(kDisplaySizeTwo);
 
-    EXPECT_EQ(mTexturePool.getPoolSize(), 0u);
+    EXPECT_EQ(mTexturePool.getMinPoolSize(), mTexturePool.getPoolSize());
     texture.reset();
     // When the texture is returned to the pool, the pool now destroys it.
-    EXPECT_EQ(mTexturePool.getPoolSize(), 0u);
+    EXPECT_EQ(mTexturePool.getMinPoolSize(), mTexturePool.getPoolSize());
 
     texture = mTexturePool.borrowTexture();
     EXPECT_EQ(kDisplaySizeTwo.getWidth(),
@@ -124,11 +132,14 @@
 }
 
 TEST_F(TexturePoolTest, freesBuffersWhenDisabled) {
+    EXPECT_EQ(mTexturePool.getPoolSize(), mTexturePool.getMinPoolSize());
+
     std::deque<std::shared_ptr<TexturePool::AutoTexture>> textures;
-    for (size_t i = 0; i < 2; i++) {
+    for (size_t i = 0; i < mTexturePool.getMinPoolSize() - 1; i++) {
         textures.emplace_back(mTexturePool.borrowTexture());
     }
 
+    EXPECT_EQ(mTexturePool.getPoolSize(), 1u);
     mTexturePool.setEnabled(false);
     EXPECT_EQ(mTexturePool.getPoolSize(), 0u);
 
@@ -137,11 +148,12 @@
 }
 
 TEST_F(TexturePoolTest, doesNotHoldBuffersWhenDisabled) {
+    EXPECT_EQ(mTexturePool.getPoolSize(), mTexturePool.getMinPoolSize());
     mTexturePool.setEnabled(false);
     EXPECT_EQ(mTexturePool.getPoolSize(), 0u);
 
     std::deque<std::shared_ptr<TexturePool::AutoTexture>> textures;
-    for (size_t i = 0; i < 2; i++) {
+    for (size_t i = 0; i < mTexturePool.getMinPoolSize() - 1; i++) {
         textures.emplace_back(mTexturePool.borrowTexture());
     }
 
@@ -150,13 +162,12 @@
     EXPECT_EQ(mTexturePool.getPoolSize(), 0u);
 }
 
-TEST_F(TexturePoolTest, genFutureWhenReEnabled) {
+TEST_F(TexturePoolTest, reallocatesWhenReEnabled) {
+    EXPECT_EQ(mTexturePool.getPoolSize(), mTexturePool.getMinPoolSize());
     mTexturePool.setEnabled(false);
     EXPECT_EQ(mTexturePool.getPoolSize(), 0u);
-    EXPECT_FALSE(mTexturePool.isGenTextureFutureValid());
     mTexturePool.setEnabled(true);
-    EXPECT_EQ(mTexturePool.getPoolSize(), 0u);
-    EXPECT_TRUE(mTexturePool.isGenTextureFutureValid());
+    EXPECT_EQ(mTexturePool.getPoolSize(), mTexturePool.getMinPoolSize());
 }
 
 } // namespace
diff --git a/services/surfaceflinger/FrontEnd/LayerHierarchy.cpp b/services/surfaceflinger/FrontEnd/LayerHierarchy.cpp
index ab4c15d..962dc09 100644
--- a/services/surfaceflinger/FrontEnd/LayerHierarchy.cpp
+++ b/services/surfaceflinger/FrontEnd/LayerHierarchy.cpp
@@ -60,9 +60,9 @@
             return;
         }
     }
-    if (traversalPath.hasRelZLoop()) {
-        LOG_ALWAYS_FATAL("Found relative z loop layerId:%d", traversalPath.invalidRelativeRootId);
-    }
+
+    LLOG_ALWAYS_FATAL_WITH_TRACE_IF(traversalPath.hasRelZLoop(), "Found relative z loop layerId:%d",
+                                    traversalPath.invalidRelativeRootId);
     for (auto& [child, childVariant] : mChildren) {
         ScopedAddToTraversalPath addChildToTraversalPath(traversalPath, child->mLayer->id,
                                                          childVariant);
@@ -104,9 +104,7 @@
                            [child](const std::pair<LayerHierarchy*, Variant>& x) {
                                return x.first == child;
                            });
-    if (it == mChildren.end()) {
-        LOG_ALWAYS_FATAL("Could not find child!");
-    }
+    LLOG_ALWAYS_FATAL_WITH_TRACE_IF(it == mChildren.end(), "Could not find child!");
     mChildren.erase(it);
 }
 
@@ -119,11 +117,8 @@
                            [hierarchy](std::pair<LayerHierarchy*, Variant>& child) {
                                return child.first == hierarchy;
                            });
-    if (it == mChildren.end()) {
-        LOG_ALWAYS_FATAL("Could not find child!");
-    } else {
-        it->second = variant;
-    }
+    LLOG_ALWAYS_FATAL_WITH_TRACE_IF(it == mChildren.end(), "Could not find child!");
+    it->second = variant;
 }
 
 const RequestedLayerState* LayerHierarchy::getLayer() const {
@@ -422,9 +417,8 @@
 LayerHierarchy* LayerHierarchyBuilder::getHierarchyFromId(uint32_t layerId, bool crashOnFailure) {
     auto it = mLayerIdToHierarchy.find(layerId);
     if (it == mLayerIdToHierarchy.end()) {
-        if (crashOnFailure) {
-            LOG_ALWAYS_FATAL("Could not find hierarchy for layer id %d", layerId);
-        }
+        LLOG_ALWAYS_FATAL_WITH_TRACE_IF(crashOnFailure, "Could not find hierarchy for layer id %d",
+                                        layerId);
         return nullptr;
     };
 
@@ -460,7 +454,7 @@
 }
 
 LayerHierarchy::TraversalPath LayerHierarchy::TraversalPath::getMirrorRoot() const {
-    LOG_ALWAYS_FATAL_IF(!isClone(), "Cannot get mirror root of a non cloned node");
+    LLOG_ALWAYS_FATAL_WITH_TRACE_IF(!isClone(), "Cannot get mirror root of a non cloned node");
     TraversalPath mirrorRootPath = *this;
     mirrorRootPath.id = mirrorRootId;
     return mirrorRootPath;
diff --git a/services/surfaceflinger/FrontEnd/LayerLifecycleManager.cpp b/services/surfaceflinger/FrontEnd/LayerLifecycleManager.cpp
index 1712137..a826ec1 100644
--- a/services/surfaceflinger/FrontEnd/LayerLifecycleManager.cpp
+++ b/services/surfaceflinger/FrontEnd/LayerLifecycleManager.cpp
@@ -45,11 +45,11 @@
     for (auto& newLayer : newLayers) {
         RequestedLayerState& layer = *newLayer.get();
         auto [it, inserted] = mIdToLayer.try_emplace(layer.id, References{.owner = layer});
-        if (!inserted) {
-            LOG_ALWAYS_FATAL("Duplicate layer id found. New layer: %s Existing layer: %s",
-                             layer.getDebugString().c_str(),
-                             it->second.owner.getDebugString().c_str());
-        }
+        LLOG_ALWAYS_FATAL_WITH_TRACE_IF(!inserted,
+                                        "Duplicate layer id found. New layer: %s Existing layer: "
+                                        "%s",
+                                        layer.getDebugString().c_str(),
+                                        it->second.owner.getDebugString().c_str());
         mAddedLayers.push_back(newLayer.get());
         mChangedLayers.push_back(newLayer.get());
         layer.parentId = linkLayer(layer.parentId, layer.id);
@@ -85,14 +85,15 @@
     }
 }
 
-void LayerLifecycleManager::onHandlesDestroyed(const std::vector<uint32_t>& destroyedHandles,
-                                               bool ignoreUnknownHandles) {
+void LayerLifecycleManager::onHandlesDestroyed(
+        const std::vector<std::pair<uint32_t, std::string /* debugName */>>& destroyedHandles,
+        bool ignoreUnknownHandles) {
     std::vector<uint32_t> layersToBeDestroyed;
-    for (const auto& layerId : destroyedHandles) {
+    for (const auto& [layerId, name] : destroyedHandles) {
         auto it = mIdToLayer.find(layerId);
         if (it == mIdToLayer.end()) {
-            LOG_ALWAYS_FATAL_IF(!ignoreUnknownHandles, "%s Layerid not found %d", __func__,
-                                layerId);
+            LLOG_ALWAYS_FATAL_WITH_TRACE_IF(!ignoreUnknownHandles, "%s Layerid not found %s[%d]",
+                                            __func__, name.c_str(), layerId);
             continue;
         }
         RequestedLayerState& layer = it->second.owner;
@@ -113,10 +114,8 @@
     for (size_t i = 0; i < layersToBeDestroyed.size(); i++) {
         uint32_t layerId = layersToBeDestroyed[i];
         auto it = mIdToLayer.find(layerId);
-        if (it == mIdToLayer.end()) {
-            LOG_ALWAYS_FATAL("%s Layer with id %d not found", __func__, layerId);
-            continue;
-        }
+        LLOG_ALWAYS_FATAL_WITH_TRACE_IF(it == mIdToLayer.end(), "%s Layer with id %d not found",
+                                        __func__, layerId);
 
         RequestedLayerState& layer = it->second.owner;
 
@@ -135,11 +134,9 @@
         auto& references = it->second.references;
         for (uint32_t linkedLayerId : references) {
             RequestedLayerState* linkedLayer = getLayerFromId(linkedLayerId);
-            if (!linkedLayer) {
-                LOG_ALWAYS_FATAL("%s Layerid reference %d not found for %d", __func__,
-                                 linkedLayerId, layer.id);
-                continue;
-            };
+            LLOG_ALWAYS_FATAL_WITH_TRACE_IF(!linkedLayer,
+                                            "%s Layerid reference %d not found for %d", __func__,
+                                            linkedLayerId, layer.id);
             if (linkedLayer->parentId == layer.id) {
                 linkedLayer->parentId = UNASSIGNED_LAYER_ID;
                 if (linkedLayer->canBeDestroyed()) {
@@ -191,17 +188,17 @@
 
             RequestedLayerState* layer = getLayerFromId(layerId);
             if (layer == nullptr) {
-                LOG_ALWAYS_FATAL_IF(!ignoreUnknownLayers, "%s Layer with layerid=%d not found",
-                                    __func__, layerId);
+                LLOG_ALWAYS_FATAL_WITH_TRACE_IF(!ignoreUnknownLayers,
+                                                "%s Layer with layerid=%d not found", __func__,
+                                                layerId);
                 continue;
             }
 
-            if (!layer->handleAlive) {
-                LOG_ALWAYS_FATAL("%s Layer's with layerid=%d) is not alive. Possible out of "
-                                 "order LayerLifecycleManager updates",
-                                 __func__, layerId);
-                continue;
-            }
+            LLOG_ALWAYS_FATAL_WITH_TRACE_IF(!layer->handleAlive,
+                                            "%s Layer's with layerid=%d) is not alive. Possible "
+                                            "out of "
+                                            "order LayerLifecycleManager updates",
+                                            __func__, layerId);
 
             if (layer->changes.get() == 0) {
                 mChangedLayers.push_back(layer);
@@ -241,7 +238,7 @@
                     RequestedLayerState* bgColorLayer = getLayerFromId(layer->bgColorLayerId);
                     layer->bgColorLayerId = UNASSIGNED_LAYER_ID;
                     bgColorLayer->parentId = unlinkLayer(bgColorLayer->parentId, bgColorLayer->id);
-                    onHandlesDestroyed({bgColorLayer->id});
+                    onHandlesDestroyed({{bgColorLayer->id, bgColorLayer->debugName}});
                 } else if (layer->bgColorLayerId != UNASSIGNED_LAYER_ID) {
                     RequestedLayerState* bgColorLayer = getLayerFromId(layer->bgColorLayerId);
                     bgColorLayer->color = layer->bgColor;
diff --git a/services/surfaceflinger/FrontEnd/LayerLifecycleManager.h b/services/surfaceflinger/FrontEnd/LayerLifecycleManager.h
index 48571bf..9aff78e 100644
--- a/services/surfaceflinger/FrontEnd/LayerLifecycleManager.h
+++ b/services/surfaceflinger/FrontEnd/LayerLifecycleManager.h
@@ -47,7 +47,8 @@
     // Ignore unknown handles when iteroping with legacy front end. In the old world, we
     // would create child layers which are not necessary with the new front end. This means
     // we will get notified for handle changes that don't exist in the new front end.
-    void onHandlesDestroyed(const std::vector<uint32_t>&, bool ignoreUnknownHandles = false);
+    void onHandlesDestroyed(const std::vector<std::pair<uint32_t, std::string /* debugName */>>&,
+                            bool ignoreUnknownHandles = false);
 
     // Detaches the layer from its relative parent to prevent a loop in the
     // layer hierarchy. This overrides the RequestedLayerState and leaves
diff --git a/services/surfaceflinger/FrontEnd/LayerLog.h b/services/surfaceflinger/FrontEnd/LayerLog.h
index 4943483..3845dfe 100644
--- a/services/surfaceflinger/FrontEnd/LayerLog.h
+++ b/services/surfaceflinger/FrontEnd/LayerLog.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "Tracing/TransactionTracing.h"
+
 // Uncomment to trace layer updates for a single layer
 // #define LOG_LAYER 1
 
@@ -27,3 +29,17 @@
 #endif
 
 #define LLOGD(LAYER_ID, x, ...) ALOGD("[%d] %s " x, (LAYER_ID), __func__, ##__VA_ARGS__);
+
+#define LLOG_ALWAYS_FATAL_WITH_TRACE(...)                                               \
+    do {                                                                                \
+        TransactionTraceWriter::getInstance().invoke(__func__, /* overwrite= */ false); \
+        LOG_ALWAYS_FATAL(##__VA_ARGS__);                                                \
+    } while (false)
+
+#define LLOG_ALWAYS_FATAL_WITH_TRACE_IF(cond, ...)                                          \
+    do {                                                                                    \
+        if (__predict_false(cond)) {                                                        \
+            TransactionTraceWriter::getInstance().invoke(__func__, /* overwrite= */ false); \
+        }                                                                                   \
+        LOG_ALWAYS_FATAL_IF(cond, ##__VA_ARGS__);                                           \
+    } while (false)
\ No newline at end of file
diff --git a/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp b/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp
index 159d0f0..da84e44 100644
--- a/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp
+++ b/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp
@@ -178,12 +178,7 @@
         info.touchableRegion.clear();
     }
 
-    const Rect roundedFrameInDisplay =
-            getInputBoundsInDisplaySpace(snapshot, inputBounds, screenToDisplay);
-    info.frameLeft = roundedFrameInDisplay.left;
-    info.frameTop = roundedFrameInDisplay.top;
-    info.frameRight = roundedFrameInDisplay.right;
-    info.frameBottom = roundedFrameInDisplay.bottom;
+    info.frame = getInputBoundsInDisplaySpace(snapshot, inputBounds, screenToDisplay);
 
     ui::Transform inputToLayer;
     inputToLayer.set(inputBounds.left, inputBounds.top);
@@ -523,12 +518,9 @@
         const Args& args, const LayerHierarchy& hierarchy,
         LayerHierarchy::TraversalPath& traversalPath, const LayerSnapshot& parentSnapshot,
         int depth) {
-    if (depth > 50) {
-        TransactionTraceWriter::getInstance().invoke("layer_builder_stack_overflow_",
-                                                     /*overwrite=*/false);
-        LOG_ALWAYS_FATAL("Cycle detected in LayerSnapshotBuilder. See "
-                         "builder_stack_overflow_transactions.winscope");
-    }
+    LLOG_ALWAYS_FATAL_WITH_TRACE_IF(depth > 50,
+                                    "Cycle detected in LayerSnapshotBuilder. See "
+                                    "builder_stack_overflow_transactions.winscope");
 
     const RequestedLayerState* layer = hierarchy.getLayer();
     LayerSnapshot* snapshot = getSnapshot(traversalPath);
@@ -675,8 +667,7 @@
     }
 
     using FrameRateCompatibility = scheduler::LayerInfo::FrameRateCompatibility;
-    if (snapshot.frameRate.rate.isValid() ||
-        snapshot.frameRate.type == FrameRateCompatibility::NoVote) {
+    if (snapshot.frameRate.isValid()) {
         // we already have a valid framerate.
         return;
     }
@@ -684,15 +675,17 @@
     // We return whether this layer or its children has a vote. We ignore ExactOrMultiple votes
     // for the same reason we are allowing touch boost for those layers. See
     // RefreshRateSelector::rankFrameRates for details.
-    const auto layerVotedWithDefaultCompatibility = childSnapshot.frameRate.rate.isValid() &&
-            childSnapshot.frameRate.type == FrameRateCompatibility::Default;
+    const auto layerVotedWithDefaultCompatibility = childSnapshot.frameRate.vote.rate.isValid() &&
+            childSnapshot.frameRate.vote.type == FrameRateCompatibility::Default;
     const auto layerVotedWithNoVote =
-            childSnapshot.frameRate.type == FrameRateCompatibility::NoVote;
-    const auto layerVotedWithExactCompatibility = childSnapshot.frameRate.rate.isValid() &&
-            childSnapshot.frameRate.type == FrameRateCompatibility::Exact;
+            childSnapshot.frameRate.vote.type == FrameRateCompatibility::NoVote;
+    const auto layerVotedWithCategory =
+            childSnapshot.frameRate.category != FrameRateCategory::Default;
+    const auto layerVotedWithExactCompatibility = childSnapshot.frameRate.vote.rate.isValid() &&
+            childSnapshot.frameRate.vote.type == FrameRateCompatibility::Exact;
 
     bool childHasValidFrameRate = layerVotedWithDefaultCompatibility || layerVotedWithNoVote ||
-            layerVotedWithExactCompatibility;
+            layerVotedWithCategory || layerVotedWithExactCompatibility;
 
     // If we don't have a valid frame rate, but the children do, we set this
     // layer as NoVote to allow the children to control the refresh rate
@@ -820,11 +813,8 @@
                 RequestedLayerState::Changes::Hierarchy) ||
         snapshot.changes.any(RequestedLayerState::Changes::FrameRate |
                              RequestedLayerState::Changes::Hierarchy)) {
-        snapshot.frameRate = (requested.requestedFrameRate.rate.isValid() ||
-                              (requested.requestedFrameRate.type ==
-                               scheduler::LayerInfo::FrameRateCompatibility::NoVote))
-                ? requested.requestedFrameRate
-                : parentSnapshot.frameRate;
+        snapshot.frameRate = requested.requestedFrameRate.isValid() ? requested.requestedFrameRate
+                                                                    : parentSnapshot.frameRate;
         snapshot.changes |= RequestedLayerState::Changes::FrameRate;
     }
 
diff --git a/services/surfaceflinger/FrontEnd/RequestedLayerState.cpp b/services/surfaceflinger/FrontEnd/RequestedLayerState.cpp
index a5d5563..de32951 100644
--- a/services/surfaceflinger/FrontEnd/RequestedLayerState.cpp
+++ b/services/surfaceflinger/FrontEnd/RequestedLayerState.cpp
@@ -24,6 +24,8 @@
 #include <private/android_filesystem_config.h>
 #include <sys/types.h>
 
+#include <scheduler/Fps.h>
+
 #include "Layer.h"
 #include "LayerCreationArgs.h"
 #include "LayerLog.h"
@@ -122,6 +124,7 @@
     dimmingEnabled = true;
     defaultFrameRateCompatibility =
             static_cast<int8_t>(scheduler::LayerInfo::FrameRateCompatibility::Default);
+    frameRateCategory = static_cast<int8_t>(FrameRateCategory::Default);
     dataspace = ui::Dataspace::V0_SRGB;
     gameMode = gui::GameMode::Unsupported;
     requestedFrameRate = {};
@@ -147,17 +150,19 @@
             ? ui::Size(externalTexture->getWidth(), externalTexture->getHeight())
             : ui::Size();
     const uint64_t oldUsageFlags = hadBuffer ? externalTexture->getUsage() : 0;
+    const bool oldBufferFormatOpaque = LayerSnapshot::isOpaqueFormat(
+            externalTexture ? externalTexture->getPixelFormat() : PIXEL_FORMAT_NONE);
 
     const bool hadSideStream = sidebandStream != nullptr;
     const layer_state_t& clientState = resolvedComposerState.state;
-    const bool hadBlur = hasBlur();
+    const bool hadSomethingToDraw = hasSomethingToDraw();
     uint64_t clientChanges = what | layer_state_t::diff(clientState);
     layer_state_t::merge(clientState);
     what = clientChanges;
     LLOGV(layerId, "requested=%" PRIu64 "flags=%" PRIu64, clientState.what, clientChanges);
 
     if (clientState.what & layer_state_t::eFlagsChanged) {
-        if ((oldFlags ^ flags) & layer_state_t::eLayerHidden) {
+        if ((oldFlags ^ flags) & (layer_state_t::eLayerHidden | layer_state_t::eLayerOpaque)) {
             changes |= RequestedLayerState::Changes::Visibility |
                     RequestedLayerState::Changes::VisibleRegion;
         }
@@ -211,6 +216,13 @@
             barrierProducerId = std::max(bufferData->producerId, barrierProducerId);
             barrierFrameNumber = std::max(bufferData->frameNumber, barrierFrameNumber);
         }
+
+        const bool newBufferFormatOpaque = LayerSnapshot::isOpaqueFormat(
+                externalTexture ? externalTexture->getPixelFormat() : PIXEL_FORMAT_NONE);
+        if (newBufferFormatOpaque != oldBufferFormatOpaque) {
+            changes |= RequestedLayerState::Changes::Visibility |
+                    RequestedLayerState::Changes::VisibleRegion;
+        }
     }
 
     if (clientState.what & layer_state_t::eSidebandStreamChanged) {
@@ -228,11 +240,10 @@
                     RequestedLayerState::Changes::VisibleRegion;
         }
     }
-    if (what & (layer_state_t::eBackgroundBlurRadiusChanged | layer_state_t::eBlurRegionsChanged)) {
-        if (hadBlur != hasBlur()) {
-            changes |= RequestedLayerState::Changes::Visibility |
-                    RequestedLayerState::Changes::VisibleRegion;
-        }
+
+    if (hadSomethingToDraw != hasSomethingToDraw()) {
+        changes |= RequestedLayerState::Changes::Visibility |
+                RequestedLayerState::Changes::VisibleRegion;
     }
     if (clientChanges & layer_state_t::HIERARCHY_CHANGES)
         changes |= RequestedLayerState::Changes::Hierarchy;
@@ -318,8 +329,14 @@
                 Layer::FrameRate::convertCompatibility(clientState.frameRateCompatibility);
         const auto strategy = Layer::FrameRate::convertChangeFrameRateStrategy(
                 clientState.changeFrameRateStrategy);
-        requestedFrameRate =
-                Layer::FrameRate(Fps::fromValue(clientState.frameRate), compatibility, strategy);
+        requestedFrameRate.vote =
+                Layer::FrameRate::FrameRateVote(Fps::fromValue(clientState.frameRate),
+                                                compatibility, strategy);
+        changes |= RequestedLayerState::Changes::FrameRate;
+    }
+    if (clientState.what & layer_state_t::eFrameRateCategoryChanged) {
+        const auto category = Layer::FrameRate::convertCategory(clientState.frameRateCategory);
+        requestedFrameRate.category = category;
         changes |= RequestedLayerState::Changes::FrameRate;
     }
 }
@@ -579,6 +596,12 @@
     return externalTexture && externalTexture->getUsage() & GRALLOC_USAGE_PROTECTED;
 }
 
+bool RequestedLayerState::hasSomethingToDraw() const {
+    return externalTexture != nullptr || sidebandStream != nullptr || shadowRadius > 0.f ||
+            backgroundBlurRadius > 0 || blurRegions.size() > 0 ||
+            (color.r >= 0.0_hf && color.g >= 0.0_hf && color.b >= 0.0_hf);
+}
+
 void RequestedLayerState::clearChanges() {
     what = 0;
     changes.clear();
diff --git a/services/surfaceflinger/FrontEnd/RequestedLayerState.h b/services/surfaceflinger/FrontEnd/RequestedLayerState.h
index 8eff22b..09f33de 100644
--- a/services/surfaceflinger/FrontEnd/RequestedLayerState.h
+++ b/services/surfaceflinger/FrontEnd/RequestedLayerState.h
@@ -87,6 +87,7 @@
     bool backpressureEnabled() const;
     bool isSimpleBufferUpdate(const layer_state_t&) const;
     bool isProtected() const;
+    bool hasSomethingToDraw() const;
 
     // Layer serial number.  This gives layers an explicit ordering, so we
     // have a stable sort order when their layer stack and Z-order are
diff --git a/services/surfaceflinger/FrontEnd/TransactionHandler.cpp b/services/surfaceflinger/FrontEnd/TransactionHandler.cpp
index ca7c3c2..d3d9509 100644
--- a/services/surfaceflinger/FrontEnd/TransactionHandler.cpp
+++ b/services/surfaceflinger/FrontEnd/TransactionHandler.cpp
@@ -22,6 +22,7 @@
 #include <cutils/trace.h>
 #include <utils/Log.h>
 #include <utils/Trace.h>
+#include "FrontEnd/LayerLog.h"
 
 #include "TransactionHandler.h"
 
@@ -87,8 +88,8 @@
     }
 
     auto it = mPendingTransactionQueues.find(flushState.queueWithUnsignaledBuffer);
-    LOG_ALWAYS_FATAL_IF(it == mPendingTransactionQueues.end(),
-                        "Could not find queue with unsignaled buffer!");
+    LLOG_ALWAYS_FATAL_WITH_TRACE_IF(it == mPendingTransactionQueues.end(),
+                                    "Could not find queue with unsignaled buffer!");
 
     auto& queue = it->second;
     popTransactionFromPending(transactions, flushState, queue);
diff --git a/services/surfaceflinger/FrontEnd/Update.h b/services/surfaceflinger/FrontEnd/Update.h
index e1449b6..e5cca8f 100644
--- a/services/surfaceflinger/FrontEnd/Update.h
+++ b/services/surfaceflinger/FrontEnd/Update.h
@@ -46,7 +46,7 @@
     std::vector<LayerCreatedState> layerCreatedStates;
     std::vector<std::unique_ptr<frontend::RequestedLayerState>> newLayers;
     std::vector<LayerCreationArgs> layerCreationArgs;
-    std::vector<uint32_t> destroyedHandles;
+    std::vector<std::pair<uint32_t, std::string /* debugName */>> destroyedHandles;
 };
 
 } // namespace android::surfaceflinger::frontend
diff --git a/services/surfaceflinger/HdrLayerInfoReporter.cpp b/services/surfaceflinger/HdrLayerInfoReporter.cpp
index 9eefbe4..2788332 100644
--- a/services/surfaceflinger/HdrLayerInfoReporter.cpp
+++ b/services/surfaceflinger/HdrLayerInfoReporter.cpp
@@ -18,14 +18,22 @@
 #define LOG_TAG "HdrLayerInfoReporter"
 #define ATRACE_TAG ATRACE_TAG_GRAPHICS
 
+#include <android-base/stringprintf.h>
+#include <inttypes.h>
 #include <utils/Trace.h>
 
 #include "HdrLayerInfoReporter.h"
 
 namespace android {
 
+using base::StringAppendF;
+
 void HdrLayerInfoReporter::dispatchHdrLayerInfo(const HdrLayerInfo& info) {
     ATRACE_CALL();
+    if (mHdrInfoHistory.size() == 0 || mHdrInfoHistory.back().info != info) {
+        mHdrInfoHistory.next() = EventHistoryEntry{info};
+    }
+
     std::vector<sp<gui::IHdrLayerInfoListener>> toInvoke;
     {
         std::scoped_lock lock(mMutex);
@@ -62,4 +70,15 @@
     mListeners.erase(wp<IBinder>(IInterface::asBinder(listener)));
 }
 
+void HdrLayerInfoReporter::dump(std::string& result) const {
+    for (size_t i = 0; i < mHdrInfoHistory.size(); i++) {
+        const auto& event = mHdrInfoHistory[i];
+        const auto& info = event.info;
+        StringAppendF(&result,
+                      "%" PRId64 ": numHdrLayers(%d), size(%dx%d), flags(%X), desiredRatio(%.2f)\n",
+                      event.timestamp, info.numberOfHdrLayers, info.maxW, info.maxH, info.flags,
+                      info.maxDesiredHdrSdrRatio);
+    }
+}
+
 } // namespace android
\ No newline at end of file
diff --git a/services/surfaceflinger/HdrLayerInfoReporter.h b/services/surfaceflinger/HdrLayerInfoReporter.h
index bf7c775..614f33f 100644
--- a/services/surfaceflinger/HdrLayerInfoReporter.h
+++ b/services/surfaceflinger/HdrLayerInfoReporter.h
@@ -19,9 +19,11 @@
 #include <android-base/thread_annotations.h>
 #include <android/gui/IHdrLayerInfoListener.h>
 #include <binder/IBinder.h>
+#include <utils/Timers.h>
 
 #include <unordered_map>
 
+#include "Utils/RingBuffer.h"
 #include "WpHash.h"
 
 namespace android {
@@ -79,6 +81,8 @@
         return !mListeners.empty();
     }
 
+    void dump(std::string& result) const;
+
 private:
     mutable std::mutex mMutex;
 
@@ -88,6 +92,17 @@
     };
 
     std::unordered_map<wp<IBinder>, TrackedListener, WpHash> mListeners GUARDED_BY(mMutex);
+
+    struct EventHistoryEntry {
+        nsecs_t timestamp = -1;
+        HdrLayerInfo info;
+
+        EventHistoryEntry() {}
+
+        EventHistoryEntry(const HdrLayerInfo& info) : info(info) { timestamp = systemTime(); }
+    };
+
+    utils::RingBuffer<EventHistoryEntry, 32> mHdrInfoHistory;
 };
 
 } // namespace android
\ No newline at end of file
diff --git a/services/surfaceflinger/HdrSdrRatioOverlay.cpp b/services/surfaceflinger/HdrSdrRatioOverlay.cpp
index 2c0f518..186e878 100644
--- a/services/surfaceflinger/HdrSdrRatioOverlay.cpp
+++ b/services/surfaceflinger/HdrSdrRatioOverlay.cpp
@@ -42,28 +42,37 @@
 }
 
 sp<GraphicBuffer> HdrSdrRatioOverlay::draw(float currentHdrSdrRatio, SkColor color,
-                                           ui::Transform::RotationFlags rotation) {
-    SkMatrix canvasTransform = SkMatrix();
-    const auto [bufferWidth, bufferHeight] = [&]() -> std::pair<int, int> {
-        switch (rotation) {
-            case ui::Transform::ROT_90:
-                canvasTransform.setTranslate(kBufferHeight, 0);
-                canvasTransform.preRotate(90.f);
-                return {kBufferHeight, kBufferWidth};
-            case ui::Transform::ROT_270:
-                canvasTransform.setRotate(270.f, kBufferWidth / 2.f, kBufferWidth / 2.f);
-                return {kBufferHeight, kBufferWidth};
-            default:
-                return {kBufferWidth, kBufferHeight};
-        }
-    }();
+                                           ui::Transform::RotationFlags rotation,
+                                           sp<GraphicBuffer>& ringBuffer) {
+    const int32_t bufferWidth = kBufferWidth;
+    const int32_t bufferHeight = kBufferWidth;
 
     const auto kUsageFlags = static_cast<uint64_t>(
             GRALLOC_USAGE_SW_WRITE_RARELY | GRALLOC_USAGE_HW_COMPOSER | GRALLOC_USAGE_HW_TEXTURE);
-    sp<GraphicBuffer> buffer =
-            sp<GraphicBuffer>::make(static_cast<uint32_t>(bufferWidth),
-                                    static_cast<uint32_t>(bufferHeight), HAL_PIXEL_FORMAT_RGBA_8888,
-                                    1u, kUsageFlags, "HdrSdrRatioOverlay");
+
+    // ring buffers here to do double-buffered rendering to avoid
+    // possible tearing and also to reduce memory take-up.
+    if (ringBuffer == nullptr) {
+        ringBuffer = sp<GraphicBuffer>::make(static_cast<uint32_t>(bufferWidth),
+                                             static_cast<uint32_t>(bufferHeight),
+                                             HAL_PIXEL_FORMAT_RGBA_8888, 1u, kUsageFlags,
+                                             "HdrSdrRatioOverlayBuffer");
+    }
+
+    auto& buffer = ringBuffer;
+
+    SkMatrix canvasTransform = SkMatrix();
+    switch (rotation) {
+        case ui::Transform::ROT_90:
+            canvasTransform.setTranslate(bufferHeight, 0);
+            canvasTransform.preRotate(90.f);
+            break;
+        case ui::Transform::ROT_270:
+            canvasTransform.setRotate(270.f, bufferWidth / 2.f, bufferWidth / 2.f);
+            break;
+        default:
+            break;
+    }
 
     const status_t bufferStatus = buffer->initCheck();
     LOG_ALWAYS_FATAL_IF(bufferStatus != OK, "HdrSdrRatioOverlay: Buffer failed to allocate: %d",
@@ -163,13 +172,13 @@
 
     const SkColor color = colorBase.toSkColor();
 
-    auto buffer = draw(currentHdrSdrRatio, color, transformHint);
+    auto buffer = draw(currentHdrSdrRatio, color, transformHint, mRingBuffer[mIndex]);
+    mIndex = (mIndex + 1) % 2;
     return buffer;
 }
 
 void HdrSdrRatioOverlay::animate() {
     if (!std::isfinite(mCurrentHdrSdrRatio) || mCurrentHdrSdrRatio < 1.0f) return;
-
     SurfaceComposerClient::Transaction()
             .setBuffer(mSurfaceControl->get(), getOrCreateBuffers(mCurrentHdrSdrRatio))
             .apply();
diff --git a/services/surfaceflinger/HdrSdrRatioOverlay.h b/services/surfaceflinger/HdrSdrRatioOverlay.h
index 8a2586e..69f95ec 100644
--- a/services/surfaceflinger/HdrSdrRatioOverlay.h
+++ b/services/surfaceflinger/HdrSdrRatioOverlay.h
@@ -35,11 +35,15 @@
 private:
     float mCurrentHdrSdrRatio = 1.f;
 
-    static sp<GraphicBuffer> draw(float currentHdrSdrRatio, SkColor, ui::Transform::RotationFlags);
+    static sp<GraphicBuffer> draw(float currentHdrSdrRatio, SkColor, ui::Transform::RotationFlags,
+                                  sp<GraphicBuffer>& ringBufer);
     static void drawNumber(float number, int left, SkColor, SkCanvas&);
 
     const sp<GraphicBuffer> getOrCreateBuffers(float currentHdrSdrRatio);
 
     const std::unique_ptr<SurfaceControlHolder> mSurfaceControl;
+
+    size_t mIndex = 0;
+    std::array<sp<GraphicBuffer>, 2> mRingBuffer;
 };
 } // namespace android
diff --git a/services/surfaceflinger/Layer.cpp b/services/surfaceflinger/Layer.cpp
index 93ebc8c..cdf7cff 100644
--- a/services/surfaceflinger/Layer.cpp
+++ b/services/surfaceflinger/Layer.cpp
@@ -27,7 +27,6 @@
 
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
-#include <android/native_window.h>
 #include <binder/IPCThreadState.h>
 #include <compositionengine/CompositionEngine.h>
 #include <compositionengine/Display.h>
@@ -50,10 +49,10 @@
 #include <stdlib.h>
 #include <sys/types.h>
 #include <system/graphics-base-v1.0.h>
-#include <ui/DataspaceUtils.h>
 #include <ui/DebugUtils.h>
 #include <ui/FloatRect.h>
 #include <ui/GraphicBuffer.h>
+#include <ui/HdrRenderTypeUtils.h>
 #include <ui/PixelFormat.h>
 #include <ui/Rect.h>
 #include <ui/Transform.h>
@@ -101,7 +100,7 @@
     using FrameRateCompatibility = TimeStats::SetFrameRateVote::FrameRateCompatibility;
     using Seamlessness = TimeStats::SetFrameRateVote::Seamlessness;
     const auto frameRateCompatibility = [frameRate] {
-        switch (frameRate.type) {
+        switch (frameRate.vote.type) {
             case Layer::FrameRateCompatibility::Default:
                 return FrameRateCompatibility::Default;
             case Layer::FrameRateCompatibility::ExactOrMultiple:
@@ -112,7 +111,7 @@
     }();
 
     const auto seamlessness = [frameRate] {
-        switch (frameRate.seamlessness) {
+        switch (frameRate.vote.seamlessness) {
             case scheduler::Seamlessness::OnlySeamless:
                 return Seamlessness::ShouldBeSeamless;
             case scheduler::Seamlessness::SeamedAndSeamless:
@@ -122,7 +121,7 @@
         }
     }();
 
-    return TimeStats::SetFrameRateVote{.frameRate = frameRate.rate.getValue(),
+    return TimeStats::SetFrameRateVote{.frameRate = frameRate.vote.rate.getValue(),
                                        .frameRateCompatibility = frameRateCompatibility,
                                        .seamlessness = seamlessness};
 }
@@ -1255,8 +1254,7 @@
 bool Layer::propagateFrameRateForLayerTree(FrameRate parentFrameRate, bool* transactionNeeded) {
     // The frame rate for layer tree is this layer's frame rate if present, or the parent frame rate
     const auto frameRate = [&] {
-        if (mDrawingState.frameRate.rate.isValid() ||
-            mDrawingState.frameRate.type == FrameRateCompatibility::NoVote) {
+        if (mDrawingState.frameRate.isValid()) {
             return mDrawingState.frameRate;
         }
 
@@ -1272,23 +1270,23 @@
                 child->propagateFrameRateForLayerTree(frameRate, transactionNeeded);
     }
 
-    // If we don't have a valid frame rate, but the children do, we set this
+    // If we don't have a valid frame rate specification, but the children do, we set this
     // layer as NoVote to allow the children to control the refresh rate
-    if (!frameRate.rate.isValid() && frameRate.type != FrameRateCompatibility::NoVote &&
-        childrenHaveFrameRate) {
+    if (!frameRate.isValid() && childrenHaveFrameRate) {
         *transactionNeeded |=
                 setFrameRateForLayerTreeLegacy(FrameRate(Fps(), FrameRateCompatibility::NoVote));
     }
 
-    // We return whether this layer ot its children has a vote. We ignore ExactOrMultiple votes for
+    // We return whether this layer or its children has a vote. We ignore ExactOrMultiple votes for
     // the same reason we are allowing touch boost for those layers. See
     // RefreshRateSelector::rankFrameRates for details.
     const auto layerVotedWithDefaultCompatibility =
-            frameRate.rate.isValid() && frameRate.type == FrameRateCompatibility::Default;
-    const auto layerVotedWithNoVote = frameRate.type == FrameRateCompatibility::NoVote;
+            frameRate.vote.rate.isValid() && frameRate.vote.type == FrameRateCompatibility::Default;
+    const auto layerVotedWithNoVote = frameRate.vote.type == FrameRateCompatibility::NoVote;
+    const auto layerVotedWithCategory = frameRate.category != FrameRateCategory::Default;
     const auto layerVotedWithExactCompatibility =
-            frameRate.rate.isValid() && frameRate.type == FrameRateCompatibility::Exact;
-    return layerVotedWithDefaultCompatibility || layerVotedWithNoVote ||
+            frameRate.vote.rate.isValid() && frameRate.vote.type == FrameRateCompatibility::Exact;
+    return layerVotedWithDefaultCompatibility || layerVotedWithNoVote || layerVotedWithCategory ||
             layerVotedWithExactCompatibility || childrenHaveFrameRate;
 }
 
@@ -1310,13 +1308,28 @@
     }
 }
 
-bool Layer::setFrameRate(FrameRate frameRate) {
-    if (mDrawingState.frameRate == frameRate) {
+bool Layer::setFrameRate(FrameRate::FrameRateVote frameRateVote) {
+    if (mDrawingState.frameRate.vote == frameRateVote) {
         return false;
     }
 
     mDrawingState.sequence++;
-    mDrawingState.frameRate = frameRate;
+    mDrawingState.frameRate.vote = frameRateVote;
+    mDrawingState.modified = true;
+
+    updateTreeHasFrameRateVote();
+
+    setTransactionFlags(eTransactionNeeded);
+    return true;
+}
+
+bool Layer::setFrameRateCategory(FrameRateCategory category) {
+    if (mDrawingState.frameRate.category == category) {
+        return false;
+    }
+
+    mDrawingState.sequence++;
+    mDrawingState.frameRate.category = category;
     mDrawingState.modified = true;
 
     updateTreeHasFrameRateVote();
@@ -1642,10 +1655,10 @@
     StringAppendF(&result, "%6.1f %6.1f %6.1f %6.1f | ", crop.left, crop.top, crop.right,
                   crop.bottom);
     const auto frameRate = getFrameRateForLayerTree();
-    if (frameRate.rate.isValid() || frameRate.type != FrameRateCompatibility::Default) {
-        StringAppendF(&result, "%s %15s %17s", to_string(frameRate.rate).c_str(),
-                      ftl::enum_string(frameRate.type).c_str(),
-                      ftl::enum_string(frameRate.seamlessness).c_str());
+    if (frameRate.vote.rate.isValid() || frameRate.vote.type != FrameRateCompatibility::Default) {
+        StringAppendF(&result, "%s %15s %17s", to_string(frameRate.vote.rate).c_str(),
+                      ftl::enum_string(frameRate.vote.type).c_str(),
+                      ftl::enum_string(frameRate.vote.seamlessness).c_str());
     } else {
         result.append(41, ' ');
     }
@@ -1677,10 +1690,10 @@
     StringAppendF(&result, "%6.1f %6.1f %6.1f %6.1f | ", crop.left, crop.top, crop.right,
                   crop.bottom);
     const auto frameRate = snapshot.frameRate;
-    if (frameRate.rate.isValid() || frameRate.type != FrameRateCompatibility::Default) {
-        StringAppendF(&result, "%s %15s %17s", to_string(frameRate.rate).c_str(),
-                      ftl::enum_string(frameRate.type).c_str(),
-                      ftl::enum_string(frameRate.seamlessness).c_str());
+    if (frameRate.vote.rate.isValid() || frameRate.vote.type != FrameRateCompatibility::Default) {
+        StringAppendF(&result, "%s %15s %17s", to_string(frameRate.vote.rate).c_str(),
+                      ftl::enum_string(frameRate.vote.type).c_str(),
+                      ftl::enum_string(frameRate.vote.seamlessness).c_str());
     } else {
         result.append(41, ' ');
     }
@@ -2389,11 +2402,7 @@
         info.touchableRegion.clear();
     }
 
-    const Rect roundedFrameInDisplay = getInputBoundsInDisplaySpace(inputBounds, screenToDisplay);
-    info.frameLeft = roundedFrameInDisplay.left;
-    info.frameTop = roundedFrameInDisplay.top;
-    info.frameRight = roundedFrameInDisplay.right;
-    info.frameBottom = roundedFrameInDisplay.bottom;
+    info.frame = getInputBoundsInDisplaySpace(inputBounds, screenToDisplay);
 
     ui::Transform inputToLayer;
     inputToLayer.set(inputBounds.left, inputBounds.top);
@@ -2814,36 +2823,6 @@
     layer->mDrawingParent = sp<Layer>::fromExisting(this);
 }
 
-Layer::FrameRateCompatibility Layer::FrameRate::convertCompatibility(int8_t compatibility) {
-    switch (compatibility) {
-        case ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT:
-            return FrameRateCompatibility::Default;
-        case ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE:
-            return FrameRateCompatibility::ExactOrMultiple;
-        case ANATIVEWINDOW_FRAME_RATE_EXACT:
-            return FrameRateCompatibility::Exact;
-        case ANATIVEWINDOW_FRAME_RATE_MIN:
-            return FrameRateCompatibility::Min;
-        case ANATIVEWINDOW_FRAME_RATE_NO_VOTE:
-            return FrameRateCompatibility::NoVote;
-        default:
-            LOG_ALWAYS_FATAL("Invalid frame rate compatibility value %d", compatibility);
-            return FrameRateCompatibility::Default;
-    }
-}
-
-scheduler::Seamlessness Layer::FrameRate::convertChangeFrameRateStrategy(int8_t strategy) {
-    switch (strategy) {
-        case ANATIVEWINDOW_CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS:
-            return Seamlessness::OnlySeamless;
-        case ANATIVEWINDOW_CHANGE_FRAME_RATE_ALWAYS:
-            return Seamlessness::SeamedAndSeamless;
-        default:
-            LOG_ALWAYS_FATAL("Invalid change frame sate strategy value %d", strategy);
-            return Seamlessness::Default;
-    }
-}
-
 bool Layer::isInternalDisplayOverlay() const {
     const State& s(mDrawingState);
     if (s.flags & layer_state_t::eLayerSkipScreenshot) {
@@ -3233,6 +3212,14 @@
     }
 
     mDrawingState.releaseBufferEndpoint = bufferData.releaseBufferEndpoint;
+
+    // If the layer had been updated a TextureView, this would make sure the present time could be
+    // same to TextureView update when it's a small dirty, and get the correct heuristic rate.
+    if (mFlinger->mScheduler->supportSmallDirtyDetection()) {
+        if (mDrawingState.useVsyncIdForRefreshRateSelection) {
+            mUsedVsyncIdForRefreshRateSelection = true;
+        }
+    }
     return true;
 }
 
@@ -3255,10 +3242,38 @@
                             mDrawingState.latchedVsyncId);
             if (prediction.has_value()) {
                 ATRACE_FORMAT_INSTANT("predictedPresentTime");
+                mMaxTimeForUseVsyncId = prediction->presentTime +
+                        scheduler::LayerHistory::kMaxPeriodForHistory.count();
                 return prediction->presentTime;
             }
         }
 
+        if (!mFlinger->mScheduler->supportSmallDirtyDetection()) {
+            return static_cast<nsecs_t>(0);
+        }
+
+        // If the layer is not an application and didn't set an explicit rate or desiredPresentTime,
+        // return "0" to tell the layer history that it will use the max refresh rate without
+        // calculating the adaptive rate.
+        if (mWindowType != WindowInfo::Type::APPLICATION &&
+            mWindowType != WindowInfo::Type::BASE_APPLICATION) {
+            return static_cast<nsecs_t>(0);
+        }
+
+        // Return the valid present time only when the layer potentially updated a TextureView so
+        // LayerHistory could heuristically calculate the rate if the UI is continually updating.
+        if (mUsedVsyncIdForRefreshRateSelection) {
+            const auto prediction =
+                    mFlinger->mFrameTimeline->getTokenManager()->getPredictionsForToken(
+                            mDrawingState.latchedVsyncId);
+            if (prediction.has_value()) {
+                if (mMaxTimeForUseVsyncId >= prediction->presentTime) {
+                    return prediction->presentTime;
+                }
+                mUsedVsyncIdForRefreshRateSelection = false;
+            }
+        }
+
         return static_cast<nsecs_t>(0);
     }();
 
@@ -3318,6 +3333,7 @@
     mDrawingState.surfaceDamageRegion = surfaceDamage;
     mDrawingState.modified = true;
     setTransactionFlags(eTransactionNeeded);
+    setIsSmallDirty();
     return true;
 }
 
@@ -4356,11 +4372,24 @@
     mLastLatchTime = latchTime;
 }
 
-// ---------------------------------------------------------------------------
+void Layer::setIsSmallDirty() {
+    if (!mFlinger->mScheduler->supportSmallDirtyDetection()) {
+        return;
+    }
 
-std::ostream& operator<<(std::ostream& stream, const Layer::FrameRate& rate) {
-    return stream << "{rate=" << rate.rate << " type=" << ftl::enum_string(rate.type)
-                  << " seamlessness=" << ftl::enum_string(rate.seamlessness) << '}';
+    if (mWindowType != WindowInfo::Type::APPLICATION &&
+        mWindowType != WindowInfo::Type::BASE_APPLICATION) {
+        return;
+    }
+    Rect bounds = mDrawingState.surfaceDamageRegion.getBounds();
+    if (!bounds.isValid()) {
+        return;
+    }
+
+    // If the damage region is a small dirty, this could give the hint for the layer history that
+    // it could suppress the heuristic rate when calculating.
+    mSmallDirty = mFlinger->mScheduler->isSmallDirtyArea(mOwnerUid,
+                                                         bounds.getWidth() * bounds.getHeight());
 }
 
 } // namespace android
diff --git a/services/surfaceflinger/Layer.h b/services/surfaceflinger/Layer.h
index 8a65d9d..7b6c56b 100644
--- a/services/surfaceflinger/Layer.h
+++ b/services/surfaceflinger/Layer.h
@@ -776,7 +776,8 @@
      */
     Rect getCroppedBufferSize(const Layer::State& s) const;
 
-    bool setFrameRate(FrameRate);
+    bool setFrameRate(FrameRate::FrameRateVote);
+    bool setFrameRateCategory(FrameRateCategory);
 
     virtual void setFrameTimelineInfoForBuffer(const FrameTimelineInfo& /*info*/) {}
     void setFrameTimelineVsyncForBufferTransaction(const FrameTimelineInfo& info, nsecs_t postTime);
@@ -840,6 +841,14 @@
     mutable bool contentDirty{false};
     Region surfaceDamageRegion;
 
+    // True when the surfaceDamageRegion is recognized as a small area update.
+    bool mSmallDirty{false};
+    // Used to check if mUsedVsyncIdForRefreshRateSelection should be expired when it stop updating.
+    nsecs_t mMaxTimeForUseVsyncId = 0;
+    // True when DrawState.useVsyncIdForRefreshRateSelection previously set to true during updating
+    // buffer.
+    bool mUsedVsyncIdForRefreshRateSelection{false};
+
     // Layer serial number.  This gives layers an explicit ordering, so we
     // have a stable sort order when their layer stack and Z-order are
     // the same.
@@ -902,6 +911,7 @@
                 .transform = getTransform(),
                 .setFrameRateVote = getFrameRateForLayerTree(),
                 .frameRateSelectionPriority = getFrameRateSelectionPriority(),
+                .isSmallDirty = mSmallDirty,
         };
     };
     bool hasBuffer() const { return mBufferInfo.mBuffer != nullptr; }
@@ -916,6 +926,9 @@
     // Exposed so SurfaceFlinger can assert that it's held
     const sp<SurfaceFlinger> mFlinger;
 
+    // Check if the damage region is a small dirty.
+    void setIsSmallDirty();
+
 protected:
     // For unit tests
     friend class TestableSurfaceFlinger;
diff --git a/services/surfaceflinger/LayerProtoHelper.cpp b/services/surfaceflinger/LayerProtoHelper.cpp
index 1c7581b..341f041 100644
--- a/services/surfaceflinger/LayerProtoHelper.cpp
+++ b/services/surfaceflinger/LayerProtoHelper.cpp
@@ -185,8 +185,8 @@
     static_assert(std::is_same_v<U, int32_t>);
     proto->set_layout_params_type(static_cast<U>(inputInfo.layoutParamsType));
 
-    LayerProtoHelper::writeToProto({inputInfo.frameLeft, inputInfo.frameTop, inputInfo.frameRight,
-                                    inputInfo.frameBottom},
+    LayerProtoHelper::writeToProto({inputInfo.frame.left, inputInfo.frame.top,
+                                    inputInfo.frame.right, inputInfo.frame.bottom},
                                    [&]() { return proto->mutable_frame(); });
     LayerProtoHelper::writeToProto(inputInfo.touchableRegion,
                                    [&]() { return proto->mutable_touchable_region(); });
diff --git a/services/surfaceflinger/Scheduler/LayerHistory.cpp b/services/surfaceflinger/Scheduler/LayerHistory.cpp
index 5d00a26..c92e670 100644
--- a/services/surfaceflinger/Scheduler/LayerHistory.cpp
+++ b/services/surfaceflinger/Scheduler/LayerHistory.cpp
@@ -40,8 +40,9 @@
 namespace {
 
 bool isLayerActive(const LayerInfo& info, nsecs_t threshold) {
-    // Layers with an explicit vote are always kept active
-    if (info.getSetFrameRateVote().rate.isValid()) {
+    // Layers with an explicit frame rate or frame rate category are always kept active,
+    // but ignore NoVote/NoPreference.
+    if (info.getSetFrameRateVote().isValid() && !info.getSetFrameRateVote().isNoVote()) {
         return true;
     }
 
@@ -70,6 +71,7 @@
     traceType(LayerHistory::LayerVoteType::ExplicitExact, fps);
     traceType(LayerHistory::LayerVoteType::Min, 1);
     traceType(LayerHistory::LayerVoteType::Max, 1);
+    traceType(LayerHistory::LayerVoteType::ExplicitCategory, 1);
 
     ALOGD("%s: %s @ %d Hz", __FUNCTION__, info.getName().c_str(), fps);
 }
@@ -171,27 +173,32 @@
               layerFocused ? "" : "not");
 
         ATRACE_FORMAT("%s", info->getName().c_str());
-        const auto vote = info->getRefreshRateVote(selector, now);
-        // Skip NoVote layer as those don't have any requirements
-        if (vote.type == LayerVoteType::NoVote) {
-            continue;
-        }
+        const auto votes = info->getRefreshRateVote(selector, now);
+        for (LayerInfo::LayerVote vote : votes) {
+            if (vote.isNoVote()) {
+                continue;
+            }
 
-        // Compute the layer's position on the screen
-        const Rect bounds = Rect(info->getBounds());
-        const ui::Transform transform = info->getTransform();
-        constexpr bool roundOutwards = true;
-        Rect transformed = transform.transform(bounds, roundOutwards);
+            // Compute the layer's position on the screen
+            const Rect bounds = Rect(info->getBounds());
+            const ui::Transform transform = info->getTransform();
+            constexpr bool roundOutwards = true;
+            Rect transformed = transform.transform(bounds, roundOutwards);
 
-        const float layerArea = transformed.getWidth() * transformed.getHeight();
-        float weight = mDisplayArea ? layerArea / mDisplayArea : 0.0f;
-        ATRACE_FORMAT_INSTANT("%s %s (%d%)", ftl::enum_string(vote.type).c_str(),
-                              to_string(vote.fps).c_str(), weight * 100);
-        summary.push_back({info->getName(), info->getOwnerUid(), vote.type, vote.fps,
-                           vote.seamlessness, weight, layerFocused});
+            const float layerArea = transformed.getWidth() * transformed.getHeight();
+            float weight = mDisplayArea ? layerArea / mDisplayArea : 0.0f;
+            const std::string categoryString = vote.category == FrameRateCategory::Default
+                    ? base::StringPrintf("category=%s", ftl::enum_string(vote.category).c_str())
+                    : "";
+            ATRACE_FORMAT_INSTANT("%s %s %s (%d%)", ftl::enum_string(vote.type).c_str(),
+                                  to_string(vote.fps).c_str(), categoryString.c_str(),
+                                  weight * 100);
+            summary.push_back({info->getName(), info->getOwnerUid(), vote.type, vote.fps,
+                               vote.seamlessness, vote.category, weight, layerFocused});
 
-        if (CC_UNLIKELY(mTraceEnabled)) {
-            trace(*info, vote.type, vote.fps.getIntValue());
+            if (CC_UNLIKELY(mTraceEnabled)) {
+                trace(*info, vote.type, vote.fps.getIntValue());
+            }
         }
     }
 
@@ -228,7 +235,7 @@
             // Set layer vote if set
             const auto frameRate = info->getSetFrameRateVote();
             const auto voteType = [&]() {
-                switch (frameRate.type) {
+                switch (frameRate.vote.type) {
                     case Layer::FrameRateCompatibility::Default:
                         return LayerVoteType::ExplicitDefault;
                     case Layer::FrameRateCompatibility::Min:
@@ -242,9 +249,10 @@
                 }
             }();
 
-            if (frameRate.rate.isValid() || voteType == LayerVoteType::NoVote) {
+            if (frameRate.isValid()) {
                 const auto type = info->isVisible() ? voteType : LayerVoteType::NoVote;
-                info->setLayerVote({type, frameRate.rate, frameRate.seamlessness});
+                info->setLayerVote({type, frameRate.vote.rate, frameRate.vote.seamlessness,
+                                    frameRate.category});
             } else {
                 info->resetLayerVote();
             }
@@ -298,4 +306,11 @@
     return {LayerStatus::NotFound, nullptr};
 }
 
+bool LayerHistory::isSmallDirtyArea(uint32_t dirtyArea, float threshold) const {
+    const float ratio = (float)dirtyArea / mDisplayArea;
+    const bool isSmallDirty = ratio <= threshold;
+    ATRACE_FORMAT_INSTANT("small dirty=%s, ratio=%.3f", isSmallDirty ? "true" : "false", ratio);
+    return isSmallDirty;
+}
+
 } // namespace android::scheduler
diff --git a/services/surfaceflinger/Scheduler/LayerHistory.h b/services/surfaceflinger/Scheduler/LayerHistory.h
index d083fa2..5750ea7 100644
--- a/services/surfaceflinger/Scheduler/LayerHistory.h
+++ b/services/surfaceflinger/Scheduler/LayerHistory.h
@@ -43,6 +43,7 @@
 class LayerHistory {
 public:
     using LayerVoteType = RefreshRateSelector::LayerVoteType;
+    static constexpr std::chrono::nanoseconds kMaxPeriodForHistory = 1s;
 
     LayerHistory();
     ~LayerHistory();
@@ -84,6 +85,8 @@
     // return the frames per second of the layer with the given sequence id.
     float getLayerFramerate(nsecs_t now, int32_t id) const;
 
+    bool isSmallDirtyArea(uint32_t dirtyArea, float threshold) const;
+
 private:
     friend class LayerHistoryTest;
     friend class TestableScheduler;
diff --git a/services/surfaceflinger/Scheduler/LayerInfo.cpp b/services/surfaceflinger/Scheduler/LayerInfo.cpp
index bae3739..e4df494 100644
--- a/services/surfaceflinger/Scheduler/LayerInfo.cpp
+++ b/services/surfaceflinger/Scheduler/LayerInfo.cpp
@@ -26,10 +26,12 @@
 #include <algorithm>
 #include <utility>
 
+#include <android/native_window.h>
 #include <cutils/compiler.h>
 #include <cutils/trace.h>
 #include <ftl/enum.h>
 #include <gui/TraceUtils.h>
+#include <system/window.h>
 
 #undef LOG_TAG
 #define LOG_TAG "LayerInfo"
@@ -63,7 +65,8 @@
         case LayerUpdateType::Buffer:
             FrameTimeData frameTime = {.presentTime = lastPresentTime,
                                        .queueTime = mLastUpdatedTime,
-                                       .pendingModeChange = pendingModeChange};
+                                       .pendingModeChange = pendingModeChange,
+                                       .isSmallDirty = props.isSmallDirty};
             mFrameTimes.push_back(frameTime);
             if (mFrameTimes.size() > HISTORY_SIZE) {
                 mFrameTimes.pop_front();
@@ -99,11 +102,15 @@
     // classification.
     bool isFrequent = true;
     bool isInfrequent = true;
+    int32_t smallDirtyCount = 0;
     const auto n = mFrameTimes.size() - 1;
     for (size_t i = 0; i < kFrequentLayerWindowSize - 1; i++) {
         if (mFrameTimes[n - i].queueTime - mFrameTimes[n - i - 1].queueTime <
             kMaxPeriodForFrequentLayerNs.count()) {
             isInfrequent = false;
+            if (mFrameTimes[n - i].presentTime == 0 && mFrameTimes[n - i].isSmallDirty) {
+                smallDirtyCount++;
+            }
         } else {
             isFrequent = false;
         }
@@ -113,7 +120,8 @@
         // If the layer was previously inconclusive, we clear
         // the history as indeterminate layers changed to frequent,
         // and we should not look at the stale data.
-        return {isFrequent, isFrequent && !mIsFrequencyConclusive, /* isConclusive */ true};
+        return {isFrequent, isFrequent && !mIsFrequencyConclusive, /* isConclusive */ true,
+                /* isSmallDirty */ smallDirtyCount >= kNumSmallDirtyThreshold};
     }
 
     // If we can't determine whether the layer is frequent or not, we return
@@ -202,6 +210,7 @@
 
     nsecs_t totalDeltas = 0;
     int numDeltas = 0;
+    int32_t smallDirtyCount = 0;
     auto prevFrame = mFrameTimes.begin();
     for (auto it = mFrameTimes.begin() + 1; it != mFrameTimes.end(); ++it) {
         const auto currDelta = getFrameTime(*it) - getFrameTime(*prevFrame);
@@ -210,6 +219,13 @@
             continue;
         }
 
+        // If this is a small area update, we don't want to consider it for calculating the average
+        // frame time. Instead, we let the bigger frame updates to drive the calculation.
+        if (it->isSmallDirty && currDelta < kMinPeriodBetweenSmallDirtyFrames) {
+            smallDirtyCount++;
+            continue;
+        }
+
         prevFrame = it;
 
         if (currDelta > kMaxPeriodBetweenFrames) {
@@ -221,6 +237,10 @@
         numDeltas++;
     }
 
+    if (smallDirtyCount > 0) {
+        ATRACE_FORMAT_INSTANT("small dirty = %" PRIu32, smallDirtyCount);
+    }
+
     if (numDeltas == 0) {
         return std::nullopt;
     }
@@ -265,19 +285,34 @@
                                                : std::nullopt;
 }
 
-LayerInfo::LayerVote LayerInfo::getRefreshRateVote(const RefreshRateSelector& selector,
-                                                   nsecs_t now) {
+LayerInfo::RefreshRateVotes LayerInfo::getRefreshRateVote(const RefreshRateSelector& selector,
+                                                          nsecs_t now) {
     ATRACE_CALL();
+    LayerInfo::RefreshRateVotes votes;
+
     if (mLayerVote.type != LayerHistory::LayerVoteType::Heuristic) {
-        ALOGV("%s voted %d ", mName.c_str(), static_cast<int>(mLayerVote.type));
-        return mLayerVote;
+        if (mLayerVote.category != FrameRateCategory::Default) {
+            ALOGV("%s uses frame rate category: %d", mName.c_str(),
+                  static_cast<int>(mLayerVote.category));
+            votes.push_back({LayerHistory::LayerVoteType::ExplicitCategory, mLayerVote.fps,
+                             Seamlessness::Default, mLayerVote.category});
+        }
+
+        if (mLayerVote.fps.isValid() ||
+            mLayerVote.type != LayerHistory::LayerVoteType::ExplicitDefault) {
+            ALOGV("%s voted %d ", mName.c_str(), static_cast<int>(mLayerVote.type));
+            votes.push_back(mLayerVote);
+        }
+
+        return votes;
     }
 
     if (isAnimating(now)) {
         ATRACE_FORMAT_INSTANT("animating");
         ALOGV("%s is animating", mName.c_str());
         mLastRefreshRate.animating = true;
-        return {LayerHistory::LayerVoteType::Max, Fps()};
+        votes.push_back({LayerHistory::LayerVoteType::Max, Fps()});
+        return votes;
     }
 
     const LayerInfo::Frequent frequent = isFrequent(now);
@@ -288,21 +323,32 @@
         mLastRefreshRate.infrequent = true;
         // Infrequent layers vote for minimal refresh rate for
         // battery saving purposes and also to prevent b/135718869.
-        return {LayerHistory::LayerVoteType::Min, Fps()};
+        votes.push_back({LayerHistory::LayerVoteType::Min, Fps()});
+        return votes;
     }
 
     if (frequent.clearHistory) {
         clearHistory(now);
     }
 
+    // Return no vote if the latest frames are small dirty.
+    if (frequent.isSmallDirty && !mLastRefreshRate.reported.isValid()) {
+        ATRACE_FORMAT_INSTANT("NoVote (small dirty)");
+        ALOGV("%s is small dirty", mName.c_str());
+        votes.push_back({LayerHistory::LayerVoteType::NoVote, Fps()});
+        return votes;
+    }
+
     auto refreshRate = calculateRefreshRateIfPossible(selector, now);
     if (refreshRate.has_value()) {
         ALOGV("%s calculated refresh rate: %s", mName.c_str(), to_string(*refreshRate).c_str());
-        return {LayerHistory::LayerVoteType::Heuristic, refreshRate.value()};
+        votes.push_back({LayerHistory::LayerVoteType::Heuristic, refreshRate.value()});
+        return votes;
     }
 
     ALOGV("%s Max (can't resolve refresh rate)", mName.c_str());
-    return {LayerHistory::LayerVoteType::Max, Fps()};
+    votes.push_back({LayerHistory::LayerVoteType::Max, Fps()});
+    return votes;
 }
 
 const char* LayerInfo::getTraceTag(LayerHistory::LayerVoteType type) const {
@@ -391,6 +437,68 @@
     return consistent;
 }
 
+LayerInfo::FrameRateCompatibility LayerInfo::FrameRate::convertCompatibility(int8_t compatibility) {
+    switch (compatibility) {
+        case ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT:
+            return FrameRateCompatibility::Default;
+        case ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE:
+            return FrameRateCompatibility::ExactOrMultiple;
+        case ANATIVEWINDOW_FRAME_RATE_EXACT:
+            return FrameRateCompatibility::Exact;
+        case ANATIVEWINDOW_FRAME_RATE_MIN:
+            return FrameRateCompatibility::Min;
+        case ANATIVEWINDOW_FRAME_RATE_NO_VOTE:
+            return FrameRateCompatibility::NoVote;
+        default:
+            LOG_ALWAYS_FATAL("Invalid frame rate compatibility value %d", compatibility);
+            return FrameRateCompatibility::Default;
+    }
+}
+
+Seamlessness LayerInfo::FrameRate::convertChangeFrameRateStrategy(int8_t strategy) {
+    switch (strategy) {
+        case ANATIVEWINDOW_CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS:
+            return Seamlessness::OnlySeamless;
+        case ANATIVEWINDOW_CHANGE_FRAME_RATE_ALWAYS:
+            return Seamlessness::SeamedAndSeamless;
+        default:
+            LOG_ALWAYS_FATAL("Invalid change frame sate strategy value %d", strategy);
+            return Seamlessness::Default;
+    }
+}
+
+FrameRateCategory LayerInfo::FrameRate::convertCategory(int8_t category) {
+    switch (category) {
+        case ANATIVEWINDOW_FRAME_RATE_CATEGORY_DEFAULT:
+            return FrameRateCategory::Default;
+        case ANATIVEWINDOW_FRAME_RATE_CATEGORY_NO_PREFERENCE:
+            return FrameRateCategory::NoPreference;
+        case ANATIVEWINDOW_FRAME_RATE_CATEGORY_LOW:
+            return FrameRateCategory::Low;
+        case ANATIVEWINDOW_FRAME_RATE_CATEGORY_NORMAL:
+            return FrameRateCategory::Normal;
+        case ANATIVEWINDOW_FRAME_RATE_CATEGORY_HIGH:
+            return FrameRateCategory::High;
+        default:
+            LOG_ALWAYS_FATAL("Invalid frame rate category value %d", category);
+            return FrameRateCategory::Default;
+    }
+}
+
+bool LayerInfo::FrameRate::isNoVote() const {
+    return vote.type == FrameRateCompatibility::NoVote ||
+            category == FrameRateCategory::NoPreference;
+}
+
+bool LayerInfo::FrameRate::isValid() const {
+    return isNoVote() || vote.rate.isValid() || category != FrameRateCategory::Default;
+}
+
+std::ostream& operator<<(std::ostream& stream, const LayerInfo::FrameRate& rate) {
+    return stream << "{rate=" << rate.vote.rate << " type=" << ftl::enum_string(rate.vote.type)
+                  << " seamlessness=" << ftl::enum_string(rate.vote.seamlessness) << '}';
+}
+
 } // namespace android::scheduler
 
 // TODO(b/129481165): remove the #pragma below and fix conversion issues
diff --git a/services/surfaceflinger/Scheduler/LayerInfo.h b/services/surfaceflinger/Scheduler/LayerInfo.h
index c5a6057..1e08ec8 100644
--- a/services/surfaceflinger/Scheduler/LayerInfo.h
+++ b/services/surfaceflinger/Scheduler/LayerInfo.h
@@ -25,6 +25,7 @@
 #include <ui/Transform.h>
 #include <utils/Timers.h>
 
+#include <scheduler/Fps.h>
 #include <scheduler/Seamlessness.h>
 
 #include "LayerHistory.h"
@@ -57,6 +58,7 @@
     static constexpr Fps kMinFpsForFrequentLayer = 10_Hz;
     static constexpr auto kMaxPeriodForFrequentLayerNs =
             std::chrono::nanoseconds(kMinFpsForFrequentLayer.getPeriodNsecs()) + 1ms;
+    static constexpr size_t kNumSmallDirtyThreshold = 2;
 
     friend class LayerHistoryTest;
     friend class LayerInfoTest;
@@ -67,8 +69,15 @@
         LayerHistory::LayerVoteType type = LayerHistory::LayerVoteType::Heuristic;
         Fps fps;
         Seamlessness seamlessness = Seamlessness::Default;
+        // Category is in effect if fps is not specified.
+        FrameRateCategory category = FrameRateCategory::Default;
+
+        // Returns true if the layer explicitly should contribute to frame rate scoring.
+        bool isNoVote() const { return RefreshRateSelector::isNoVote(type, category); }
     };
 
+    using RefreshRateVotes = ftl::SmallVector<LayerInfo::LayerVote, 2>;
+
     // FrameRateCompatibility specifies how we should interpret the frame rate associated with
     // the layer.
     enum class FrameRateCompatibility {
@@ -87,24 +96,40 @@
         ftl_last = NoVote
     };
 
-    // Encapsulates the frame rate and compatibility of the layer. This information will be used
+    // Encapsulates the frame rate specifications of the layer. This information will be used
     // when the display refresh rate is determined.
     struct FrameRate {
         using Seamlessness = scheduler::Seamlessness;
 
-        Fps rate;
-        FrameRateCompatibility type = FrameRateCompatibility::Default;
-        Seamlessness seamlessness = Seamlessness::Default;
+        // Information related to a specific desired frame rate vote.
+        struct FrameRateVote {
+            Fps rate;
+            FrameRateCompatibility type = FrameRateCompatibility::Default;
+            Seamlessness seamlessness = Seamlessness::Default;
+
+            bool operator==(const FrameRateVote& other) const {
+                return isApproxEqual(rate, other.rate) && type == other.type &&
+                        seamlessness == other.seamlessness;
+            }
+
+            FrameRateVote() = default;
+
+            FrameRateVote(Fps rate, FrameRateCompatibility type,
+                          Seamlessness seamlessness = Seamlessness::OnlySeamless)
+                  : rate(rate), type(type), seamlessness(getSeamlessness(rate, seamlessness)) {}
+        } vote;
+
+        FrameRateCategory category = FrameRateCategory::Default;
 
         FrameRate() = default;
 
         FrameRate(Fps rate, FrameRateCompatibility type,
-                  Seamlessness seamlessness = Seamlessness::OnlySeamless)
-              : rate(rate), type(type), seamlessness(getSeamlessness(rate, seamlessness)) {}
+                  Seamlessness seamlessness = Seamlessness::OnlySeamless,
+                  FrameRateCategory category = FrameRateCategory::Default)
+              : vote(FrameRateVote(rate, type, seamlessness)), category(category) {}
 
         bool operator==(const FrameRate& other) const {
-            return isApproxEqual(rate, other.rate) && type == other.type &&
-                    seamlessness == other.seamlessness;
+            return vote == other.vote && category == other.category;
         }
 
         bool operator!=(const FrameRate& other) const { return !(*this == other); }
@@ -112,8 +137,22 @@
         // Convert an ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_* value to a
         // Layer::FrameRateCompatibility. Logs fatal if the compatibility value is invalid.
         static FrameRateCompatibility convertCompatibility(int8_t compatibility);
+
+        // Convert an ANATIVEWINDOW_CHANGE_FRAME_RATE_* value to a scheduler::Seamlessness.
+        // Logs fatal if the compatibility value is invalid.
         static scheduler::Seamlessness convertChangeFrameRateStrategy(int8_t strategy);
 
+        // Convert an ANATIVEWINDOW_FRAME_RATE_CATEGORY_* value to a FrameRateCategory.
+        // Logs fatal if the compatibility value is invalid.
+        static FrameRateCategory convertCategory(int8_t category);
+
+        // True if the FrameRate has explicit frame rate specifications.
+        bool isValid() const;
+
+        // Returns true if the FrameRate explicitly instructs to not contribute to frame rate
+        // selection.
+        bool isNoVote() const;
+
     private:
         static Seamlessness getSeamlessness(Fps rate, Seamlessness seamlessness) {
             if (!rate.isValid()) {
@@ -148,13 +187,15 @@
     void setDefaultLayerVote(LayerHistory::LayerVoteType type) { mDefaultVote = type; }
 
     // Resets the layer vote to its default.
-    void resetLayerVote() { mLayerVote = {mDefaultVote, Fps(), Seamlessness::Default}; }
+    void resetLayerVote() {
+        mLayerVote = {mDefaultVote, Fps(), Seamlessness::Default, FrameRateCategory::Default};
+    }
 
     std::string getName() const { return mName; }
 
     uid_t getOwnerUid() const { return mOwnerUid; }
 
-    LayerVote getRefreshRateVote(const RefreshRateSelector&, nsecs_t now);
+    RefreshRateVotes getRefreshRateVote(const RefreshRateSelector&, nsecs_t now);
 
     // Return the last updated time. If the present time is farther in the future than the
     // updated time, the updated time is the present time.
@@ -195,6 +236,7 @@
         nsecs_t presentTime; // desiredPresentTime, if provided
         nsecs_t queueTime;  // buffer queue time
         bool pendingModeChange;
+        bool isSmallDirty;
     };
 
     // Holds information about the calculated and reported refresh rate
@@ -259,6 +301,8 @@
         bool clearHistory;
         // Represents whether we were able to determine isFrequent conclusively
         bool isConclusive;
+        // Represents whether the latest frames are small dirty.
+        bool isSmallDirty = false;
     };
     Frequent isFrequent(nsecs_t now) const;
     bool isAnimating(nsecs_t now) const;
@@ -277,6 +321,11 @@
     // this period apart from each other, the interval between them won't be
     // taken into account when calculating average frame rate.
     static constexpr nsecs_t kMaxPeriodBetweenFrames = kMinFpsForFrequentLayer.getPeriodNsecs();
+    // Used for sanitizing the heuristic data. If frames are small dirty updating and are less
+    // than this period apart from each other, the interval between them won't be
+    // taken into account when calculating average frame rate.
+    static constexpr nsecs_t kMinPeriodBetweenSmallDirtyFrames = (60_Hz).getPeriodNsecs();
+
     LayerHistory::LayerVoteType mDefaultVote;
 
     LayerVote mLayerVote;
@@ -291,7 +340,7 @@
     std::chrono::time_point<std::chrono::steady_clock> mFrameTimeValidSince =
             std::chrono::steady_clock::now();
     static constexpr size_t HISTORY_SIZE = RefreshRateHistory::HISTORY_SIZE;
-    static constexpr std::chrono::nanoseconds HISTORY_DURATION = 1s;
+    static constexpr std::chrono::nanoseconds HISTORY_DURATION = LayerHistory::kMaxPeriodForHistory;
 
     std::unique_ptr<LayerProps> mLayerProps;
 
@@ -309,6 +358,7 @@
     ui::Transform transform;
     LayerInfo::FrameRate setFrameRateVote;
     int32_t frameRateSelectionPriority = -1;
+    bool isSmallDirty = false;
 };
 
 } // namespace scheduler
diff --git a/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp b/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp
index fb985f7..7e77bbe 100644
--- a/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp
+++ b/services/surfaceflinger/Scheduler/RefreshRateSelector.cpp
@@ -148,8 +148,8 @@
 } // namespace
 
 auto RefreshRateSelector::createFrameRateModes(
-        std::function<bool(const DisplayMode&)>&& filterModes, const FpsRange& renderRange) const
-        -> std::vector<FrameRateMode> {
+        const Policy& policy, std::function<bool(const DisplayMode&)>&& filterModes,
+        const FpsRange& renderRange) const -> std::vector<FrameRateMode> {
     struct Key {
         Fps fps;
         int32_t group;
@@ -202,11 +202,25 @@
                 ALOGV("%s: including %s (%s)", __func__, to_string(fps).c_str(),
                       to_string(mode->getFps()).c_str());
             } else {
-                // We might need to update the map as we found a lower refresh rate
-                if (isStrictlyLess(mode->getFps(), existingIter->second->second->getFps())) {
+                // If the primary physical range is a single rate, prefer to stay in that rate
+                // even if there is a lower physical refresh rate available. This would cause more
+                // cases to stay within the primary physical range
+                const Fps existingModeFps = existingIter->second->second->getFps();
+                const bool existingModeIsPrimaryRange = policy.primaryRangeIsSingleRate() &&
+                        policy.primaryRanges.physical.includes(existingModeFps);
+                const bool newModeIsPrimaryRange = policy.primaryRangeIsSingleRate() &&
+                        policy.primaryRanges.physical.includes(mode->getFps());
+                if (newModeIsPrimaryRange == existingModeIsPrimaryRange) {
+                    // We might need to update the map as we found a lower refresh rate
+                    if (isStrictlyLess(mode->getFps(), existingModeFps)) {
+                        existingIter->second = it;
+                        ALOGV("%s: changing %s (%s) as we found a lower physical rate", __func__,
+                              to_string(fps).c_str(), to_string(mode->getFps()).c_str());
+                    }
+                } else if (newModeIsPrimaryRange) {
                     existingIter->second = it;
-                    ALOGV("%s: changing %s (%s)", __func__, to_string(fps).c_str(),
-                          to_string(mode->getFps()).c_str());
+                    ALOGV("%s: changing %s (%s) to stay in the primary range", __func__,
+                          to_string(fps).c_str(), to_string(mode->getFps()).c_str());
                 }
             }
         }
@@ -275,6 +289,25 @@
     return {quotient, remainder};
 }
 
+float RefreshRateSelector::calculateNonExactMatchingDefaultLayerScoreLocked(
+        nsecs_t displayPeriod, nsecs_t layerPeriod) const {
+    // Find the actual rate the layer will render, assuming
+    // that layerPeriod is the minimal period to render a frame.
+    // For example if layerPeriod is 20ms and displayPeriod is 16ms,
+    // then the actualLayerPeriod will be 32ms, because it is the
+    // smallest multiple of the display period which is >= layerPeriod.
+    auto actualLayerPeriod = displayPeriod;
+    int multiplier = 1;
+    while (layerPeriod > actualLayerPeriod + MARGIN_FOR_PERIOD_CALCULATION) {
+        multiplier++;
+        actualLayerPeriod = displayPeriod * multiplier;
+    }
+
+    // Because of the threshold we used above it's possible that score is slightly
+    // above 1.
+    return std::min(1.0f, static_cast<float>(layerPeriod) / static_cast<float>(actualLayerPeriod));
+}
+
 float RefreshRateSelector::calculateNonExactMatchingLayerScoreLocked(const LayerRequirement& layer,
                                                                      Fps refreshRate) const {
     constexpr float kScoreForFractionalPairs = .8f;
@@ -282,22 +315,7 @@
     const auto displayPeriod = refreshRate.getPeriodNsecs();
     const auto layerPeriod = layer.desiredRefreshRate.getPeriodNsecs();
     if (layer.vote == LayerVoteType::ExplicitDefault) {
-        // Find the actual rate the layer will render, assuming
-        // that layerPeriod is the minimal period to render a frame.
-        // For example if layerPeriod is 20ms and displayPeriod is 16ms,
-        // then the actualLayerPeriod will be 32ms, because it is the
-        // smallest multiple of the display period which is >= layerPeriod.
-        auto actualLayerPeriod = displayPeriod;
-        int multiplier = 1;
-        while (layerPeriod > actualLayerPeriod + MARGIN_FOR_PERIOD_CALCULATION) {
-            multiplier++;
-            actualLayerPeriod = displayPeriod * multiplier;
-        }
-
-        // Because of the threshold we used above it's possible that score is slightly
-        // above 1.
-        return std::min(1.0f,
-                        static_cast<float>(layerPeriod) / static_cast<float>(actualLayerPeriod));
+        return calculateNonExactMatchingDefaultLayerScoreLocked(displayPeriod, layerPeriod);
     }
 
     if (layer.vote == LayerVoteType::ExplicitExactOrMultiple ||
@@ -372,6 +390,22 @@
     constexpr float kSeamedSwitchPenalty = 0.95f;
     const float seamlessness = isSeamlessSwitch ? 1.0f : kSeamedSwitchPenalty;
 
+    if (layer.vote == LayerVoteType::ExplicitCategory) {
+        if (getFrameRateCategoryRange(layer.frameRateCategory).includes(refreshRate)) {
+            return 1.f;
+        }
+
+        FpsRange categoryRange = getFrameRateCategoryRange(layer.frameRateCategory);
+        using fps_approx_ops::operator<;
+        if (refreshRate < categoryRange.min) {
+            return calculateNonExactMatchingDefaultLayerScoreLocked(refreshRate.getPeriodNsecs(),
+                                                                    categoryRange.min
+                                                                            .getPeriodNsecs());
+        }
+        return calculateNonExactMatchingDefaultLayerScoreLocked(refreshRate.getPeriodNsecs(),
+                                                                categoryRange.max.getPeriodNsecs());
+    }
+
     // If the layer wants Max, give higher score to the higher refresh rate
     if (layer.vote == LayerVoteType::Max) {
         return calculateDistanceScoreFromMax(refreshRate);
@@ -391,7 +425,8 @@
 
     // If the layer frame rate is a divisor of the refresh rate it should score
     // the highest score.
-    if (getFrameRateDivisor(refreshRate, layer.desiredRefreshRate) > 0) {
+    if (layer.desiredRefreshRate.isValid() &&
+        getFrameRateDivisor(refreshRate, layer.desiredRefreshRate) > 0) {
         return 1.0f * seamlessness;
     }
 
@@ -441,6 +476,7 @@
     int explicitDefaultVoteLayers = 0;
     int explicitExactOrMultipleVoteLayers = 0;
     int explicitExact = 0;
+    int explicitCategoryVoteLayers = 0;
     int seamedFocusedLayers = 0;
 
     for (const auto& layer : layers) {
@@ -463,6 +499,9 @@
             case LayerVoteType::ExplicitExact:
                 explicitExact++;
                 break;
+            case LayerVoteType::ExplicitCategory:
+                explicitCategoryVoteLayers++;
+                break;
             case LayerVoteType::Heuristic:
                 break;
         }
@@ -473,7 +512,8 @@
     }
 
     const bool hasExplicitVoteLayers = explicitDefaultVoteLayers > 0 ||
-            explicitExactOrMultipleVoteLayers > 0 || explicitExact > 0;
+            explicitExactOrMultipleVoteLayers > 0 || explicitExact > 0 ||
+            explicitCategoryVoteLayers > 0;
 
     const Policy* policy = getCurrentPolicyLocked();
     const auto& defaultMode = mDisplayModes.get(policy->defaultMode)->get();
@@ -500,10 +540,8 @@
     // If the primary range consists of a single refresh rate then we can only
     // move out the of range if layers explicitly request a different refresh
     // rate.
-    const bool primaryRangeIsSingleRate =
-            isApproxEqual(policy->primaryRanges.physical.min, policy->primaryRanges.physical.max);
-
-    if (!signals.touch && signals.idle && !(primaryRangeIsSingleRate && hasExplicitVoteLayers)) {
+    if (!signals.touch && signals.idle &&
+        !(policy->primaryRangeIsSingleRate() && hasExplicitVoteLayers)) {
         ALOGV("Idle");
         const auto ranking = rankFrameRates(activeMode.getGroup(), RefreshRateOrder::Ascending);
         ATRACE_FORMAT_INSTANT("%s (Idle)", to_string(ranking.front().frameRateMode.fps).c_str());
@@ -536,10 +574,11 @@
     }
 
     for (const auto& layer : layers) {
-        ALOGV("Calculating score for %s (%s, weight %.2f, desired %.2f) ", layer.name.c_str(),
-              ftl::enum_string(layer.vote).c_str(), layer.weight,
-              layer.desiredRefreshRate.getValue());
-        if (layer.vote == LayerVoteType::NoVote || layer.vote == LayerVoteType::Min) {
+        ALOGV("Calculating score for %s (%s, weight %.2f, desired %.2f, category %s) ",
+              layer.name.c_str(), ftl::enum_string(layer.vote).c_str(), layer.weight,
+              layer.desiredRefreshRate.getValue(),
+              ftl::enum_string(layer.frameRateCategory).c_str());
+        if (layer.isNoVote() || layer.vote == LayerVoteType::Min) {
             continue;
         }
 
@@ -577,8 +616,11 @@
                 continue;
             }
 
-            const bool inPrimaryRange = policy->primaryRanges.render.includes(fps);
-            if ((primaryRangeIsSingleRate || !inPrimaryRange) &&
+            const bool inPrimaryPhysicalRange =
+                    policy->primaryRanges.physical.includes(modePtr->getFps());
+            const bool inPrimaryRenderRange = policy->primaryRanges.render.includes(fps);
+            if (((policy->primaryRangeIsSingleRate() && !inPrimaryPhysicalRange) ||
+                 !inPrimaryRenderRange) &&
                 !(layer.focused &&
                   (layer.vote == LayerVoteType::ExplicitDefault ||
                    layer.vote == LayerVoteType::ExplicitExact))) {
@@ -607,6 +649,7 @@
                     case LayerVoteType::Max:
                     case LayerVoteType::ExplicitDefault:
                     case LayerVoteType::ExplicitExact:
+                    case LayerVoteType::ExplicitCategory:
                         return false;
                 }
             }(layer.vote);
@@ -689,7 +732,7 @@
         return score.overallScore == 0;
     });
 
-    if (primaryRangeIsSingleRate) {
+    if (policy->primaryRangeIsSingleRate()) {
         // If we never scored any layers, then choose the rate from the primary
         // range instead of picking a random score from the app range.
         if (noLayerScore) {
@@ -722,7 +765,8 @@
     const auto touchRefreshRates = rankFrameRates(anchorGroup, RefreshRateOrder::Descending);
     using fps_approx_ops::operator<;
 
-    if (signals.touch && explicitDefaultVoteLayers == 0 && touchBoostForExplicitExact &&
+    if (signals.touch && explicitDefaultVoteLayers == 0 && explicitCategoryVoteLayers == 0 &&
+        touchBoostForExplicitExact &&
         scores.front().frameRateMode.fps < touchRefreshRates.front().frameRateMode.fps) {
         ALOGV("Touch Boost");
         ATRACE_FORMAT_INSTANT("%s (Touch Boost [late])",
@@ -838,8 +882,10 @@
             }
 
             LOG_ALWAYS_FATAL_IF(layer->vote != LayerVoteType::ExplicitDefault &&
-                                layer->vote != LayerVoteType::ExplicitExactOrMultiple &&
-                                layer->vote != LayerVoteType::ExplicitExact);
+                                        layer->vote != LayerVoteType::ExplicitExactOrMultiple &&
+                                        layer->vote != LayerVoteType::ExplicitExact &&
+                                        layer->vote != LayerVoteType::ExplicitCategory,
+                                "Invalid layer vote type for frame rate overrides");
             for (auto& [fps, score] : scoredFrameRates) {
                 constexpr bool isSeamlessSwitch = true;
                 const auto layerScore = calculateLayerScoreLocked(*layer, fps, isSeamlessSwitch);
@@ -1236,14 +1282,14 @@
                     (supportsFrameRateOverride() || ranges.render.includes(mode.getFps()));
         };
 
-        auto frameRateModes = createFrameRateModes(filterModes, ranges.render);
+        auto frameRateModes = createFrameRateModes(*policy, filterModes, ranges.render);
         if (frameRateModes.empty()) {
             ALOGW("No matching frame rate modes for %s range. policy: %s", rangeName,
                   policy->toString().c_str());
             // TODO(b/292105422): Ideally DisplayManager should not send render ranges smaller than
             // the min supported. See b/292047939.
             //  For not we just ignore the render ranges.
-            frameRateModes = createFrameRateModes(filterModes, {});
+            frameRateModes = createFrameRateModes(*policy, filterModes, {});
         }
         LOG_ALWAYS_FATAL_IF(frameRateModes.empty(),
                             "No matching frame rate modes for %s range even after ignoring the "
@@ -1380,6 +1426,27 @@
     return mConfig.idleTimerTimeout;
 }
 
+// TODO(b/293651105): Extract category FpsRange mapping to OEM-configurable config.
+FpsRange RefreshRateSelector::getFrameRateCategoryRange(FrameRateCategory category) {
+    switch (category) {
+        case FrameRateCategory::High:
+            return FpsRange{90_Hz, 120_Hz};
+        case FrameRateCategory::Normal:
+            return FpsRange{60_Hz, 90_Hz};
+        case FrameRateCategory::Low:
+            return FpsRange{30_Hz, 60_Hz};
+        case FrameRateCategory::NoPreference:
+        case FrameRateCategory::Default:
+            LOG_ALWAYS_FATAL("Should not get fps range for frame rate category: %s",
+                             ftl::enum_string(category).c_str());
+            return FpsRange{0_Hz, 0_Hz};
+        default:
+            LOG_ALWAYS_FATAL("Invalid frame rate category for range: %s",
+                             ftl::enum_string(category).c_str());
+            return FpsRange{0_Hz, 0_Hz};
+    }
+}
+
 } // namespace android::scheduler
 
 // TODO(b/129481165): remove the #pragma below and fix conversion issues
diff --git a/services/surfaceflinger/Scheduler/RefreshRateSelector.h b/services/surfaceflinger/Scheduler/RefreshRateSelector.h
index 7af8d03..73e1d38 100644
--- a/services/surfaceflinger/Scheduler/RefreshRateSelector.h
+++ b/services/surfaceflinger/Scheduler/RefreshRateSelector.h
@@ -101,6 +101,11 @@
         }
 
         bool operator!=(const Policy& other) const { return !(*this == other); }
+
+        bool primaryRangeIsSingleRate() const {
+            return isApproxEqual(primaryRanges.physical.min, primaryRanges.physical.max);
+        }
+
         std::string toString() const;
     };
 
@@ -147,8 +152,9 @@
                                  // ExactOrMultiple compatibility
         ExplicitExact,           // Specific refresh rate that was provided by the app with
                                  // Exact compatibility
+        ExplicitCategory,        // Specific frame rate category was provided by the app
 
-        ftl_last = ExplicitExact
+        ftl_last = ExplicitCategory
     };
 
     // Captures the layer requirements for a refresh rate. This will be used to determine the
@@ -164,6 +170,8 @@
         Fps desiredRefreshRate;
         // If a seamless mode switch is required.
         Seamlessness seamlessness = Seamlessness::Default;
+        // Layer frame rate category. Superseded by desiredRefreshRate.
+        FrameRateCategory frameRateCategory = FrameRateCategory::Default;
         // Layer's weight in the range of [0, 1]. The higher the weight the more impact this layer
         // would have on choosing the refresh rate.
         float weight = 0.0f;
@@ -174,12 +182,20 @@
             return name == other.name && vote == other.vote &&
                     isApproxEqual(desiredRefreshRate, other.desiredRefreshRate) &&
                     seamlessness == other.seamlessness && weight == other.weight &&
-                    focused == other.focused;
+                    focused == other.focused && frameRateCategory == other.frameRateCategory;
         }
 
         bool operator!=(const LayerRequirement& other) const { return !(*this == other); }
+
+        bool isNoVote() const { return RefreshRateSelector::isNoVote(vote, frameRateCategory); }
     };
 
+    // Returns true if the layer explicitly instructs to not contribute to refresh rate selection.
+    // In other words, true if the layer should be ignored.
+    static bool isNoVote(LayerVoteType vote, FrameRateCategory category) {
+        return vote == LayerVoteType::NoVote || category == FrameRateCategory::NoPreference;
+    }
+
     // Global state describing signals that affect refresh rate choice.
     struct GlobalSignals {
         // Whether the user touched the screen recently. Used to apply touch boost.
@@ -344,6 +360,9 @@
                                                  Fps displayFrameRate, GlobalSignals) const
             EXCLUDES(mLock);
 
+    // Gets the FpsRange that the FrameRateCategory represents.
+    static FpsRange getFrameRateCategoryRange(FrameRateCategory category);
+
     std::optional<KernelIdleTimerController> kernelIdleTimerController() {
         return mConfig.kernelIdleTimerController;
     }
@@ -447,6 +466,11 @@
     float calculateNonExactMatchingLayerScoreLocked(const LayerRequirement&, Fps refreshRate) const
             REQUIRES(mLock);
 
+    // Calculates the score for non-exact matching layer that has LayerVoteType::ExplicitDefault.
+    float calculateNonExactMatchingDefaultLayerScoreLocked(nsecs_t displayPeriod,
+                                                           nsecs_t layerPeriod) const
+            REQUIRES(mLock);
+
     void updateDisplayModes(DisplayModes, DisplayModeId activeModeId) EXCLUDES(mLock)
             REQUIRES(kMainThreadContext);
 
@@ -468,8 +492,8 @@
     }
 
     std::vector<FrameRateMode> createFrameRateModes(
-            std::function<bool(const DisplayMode&)>&& filterModes, const FpsRange&) const
-            REQUIRES(mLock);
+            const Policy&, std::function<bool(const DisplayMode&)>&& filterModes,
+            const FpsRange&) const REQUIRES(mLock);
 
     // The display modes of the active display. The DisplayModeIterators below are pointers into
     // this container, so must be invalidated whenever the DisplayModes change. The Policy below
diff --git a/services/surfaceflinger/Scheduler/Scheduler.cpp b/services/surfaceflinger/Scheduler/Scheduler.cpp
index 5a19ec5..27c96f7 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.cpp
+++ b/services/surfaceflinger/Scheduler/Scheduler.cpp
@@ -1179,4 +1179,20 @@
     mFrameRateOverrideMappings.setPreferredRefreshRateForUid(frameRateOverride);
 }
 
+void Scheduler::updateSmallAreaDetection(
+        std::vector<std::pair<uid_t, float>>& uidThresholdMappings) {
+    mSmallAreaDetectionAllowMappings.update(uidThresholdMappings);
+}
+
+void Scheduler::setSmallAreaDetectionThreshold(uid_t uid, float threshold) {
+    mSmallAreaDetectionAllowMappings.setThesholdForUid(uid, threshold);
+}
+
+bool Scheduler::isSmallDirtyArea(uid_t uid, uint32_t dirtyArea) {
+    std::optional<float> oThreshold = mSmallAreaDetectionAllowMappings.getThresholdForUid(uid);
+    if (oThreshold) return mLayerHistory.isSmallDirtyArea(dirtyArea, oThreshold.value());
+
+    return false;
+}
+
 } // namespace android::scheduler
diff --git a/services/surfaceflinger/Scheduler/Scheduler.h b/services/surfaceflinger/Scheduler/Scheduler.h
index 85d0f9a..d65df2a 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.h
+++ b/services/surfaceflinger/Scheduler/Scheduler.h
@@ -49,6 +49,7 @@
 #include "MessageQueue.h"
 #include "OneShotTimer.h"
 #include "RefreshRateSelector.h"
+#include "SmallAreaDetectionAllowMappings.h"
 #include "Utils/Dumper.h"
 #include "VsyncModulator.h"
 
@@ -291,6 +292,13 @@
 
     void setGameModeRefreshRateForUid(FrameRateOverride);
 
+    void updateSmallAreaDetection(std::vector<std::pair<uid_t, float>>& uidThresholdMappings);
+
+    void setSmallAreaDetectionThreshold(uid_t uid, float threshold);
+
+    // Returns true if the dirty area is less than threshold.
+    bool isSmallDirtyArea(uid_t uid, uint32_t dirtyArea);
+
     // Retrieves the overridden refresh rate for a given uid.
     std::optional<Fps> getFrameRateOverride(uid_t) const EXCLUDES(mDisplayLock);
 
@@ -309,6 +317,11 @@
 
     bool updateFrameRateOverrides(GlobalSignals, Fps displayRefreshRate) EXCLUDES(mPolicyLock);
 
+    // Returns true if the small dirty detection is enabled.
+    bool supportSmallDirtyDetection() const {
+        return mFeatures.test(Feature::kSmallDirtyContentDetection);
+    }
+
 private:
     friend class TestableScheduler;
 
@@ -547,6 +560,7 @@
     static constexpr std::chrono::nanoseconds MAX_VSYNC_APPLIED_TIME = 200ms;
 
     FrameRateOverrideMappings mFrameRateOverrideMappings;
+    SmallAreaDetectionAllowMappings mSmallAreaDetectionAllowMappings;
 };
 
 } // namespace scheduler
diff --git a/services/surfaceflinger/Scheduler/SmallAreaDetectionAllowMappings.cpp b/services/surfaceflinger/Scheduler/SmallAreaDetectionAllowMappings.cpp
new file mode 100644
index 0000000..95cd5d1
--- /dev/null
+++ b/services/surfaceflinger/Scheduler/SmallAreaDetectionAllowMappings.cpp
@@ -0,0 +1,47 @@
+/*
+ * 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 <sys/types.h>
+
+#include "SmallAreaDetectionAllowMappings.h"
+
+namespace android::scheduler {
+void SmallAreaDetectionAllowMappings::update(
+        std::vector<std::pair<uid_t, float>>& uidThresholdMappings) {
+    std::lock_guard lock(mLock);
+    mMap.clear();
+    for (std::pair<uid_t, float> row : uidThresholdMappings) {
+        if (!isValidThreshold(row.second)) continue;
+
+        mMap.emplace(row.first, row.second);
+    }
+}
+
+void SmallAreaDetectionAllowMappings::setThesholdForUid(uid_t uid, float threshold) {
+    if (!isValidThreshold(threshold)) return;
+
+    std::lock_guard lock(mLock);
+    mMap.emplace(uid, threshold);
+}
+
+std::optional<float> SmallAreaDetectionAllowMappings::getThresholdForUid(uid_t uid) {
+    std::lock_guard lock(mLock);
+    const auto iter = mMap.find(uid);
+    if (iter != mMap.end()) {
+        return iter->second;
+    }
+    return std::nullopt;
+}
+} // namespace android::scheduler
diff --git a/services/surfaceflinger/Scheduler/SmallAreaDetectionAllowMappings.h b/services/surfaceflinger/Scheduler/SmallAreaDetectionAllowMappings.h
new file mode 100644
index 0000000..cbab690
--- /dev/null
+++ b/services/surfaceflinger/Scheduler/SmallAreaDetectionAllowMappings.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <android-base/thread_annotations.h>
+#include <sys/types.h>
+#include <optional>
+#include <unordered_map>
+#include <vector>
+
+namespace android::scheduler {
+class SmallAreaDetectionAllowMappings {
+    using UidThresholdMap = std::unordered_map<uid_t, float>;
+
+public:
+    void update(std::vector<std::pair<uid_t, float>>& uidThresholdMappings);
+    void setThesholdForUid(uid_t uid, float threshold) EXCLUDES(mLock);
+    std::optional<float> getThresholdForUid(uid_t uid) EXCLUDES(mLock);
+
+private:
+    static bool isValidThreshold(float threshold) { return threshold >= 0.0f && threshold <= 1.0f; }
+    mutable std::mutex mLock;
+    UidThresholdMap mMap GUARDED_BY(mLock);
+};
+} // namespace android::scheduler
diff --git a/services/surfaceflinger/Scheduler/include/scheduler/Features.h b/services/surfaceflinger/Scheduler/include/scheduler/Features.h
index 200407d..7c72ac6 100644
--- a/services/surfaceflinger/Scheduler/include/scheduler/Features.h
+++ b/services/surfaceflinger/Scheduler/include/scheduler/Features.h
@@ -28,6 +28,7 @@
     kContentDetection = 1 << 2,
     kTracePredictedVsync = 1 << 3,
     kBackpressureGpuComposition = 1 << 4,
+    kSmallDirtyContentDetection = 1 << 5,
 };
 
 using FeatureFlags = ftl::Flags<Feature>;
diff --git a/services/surfaceflinger/Scheduler/include/scheduler/Fps.h b/services/surfaceflinger/Scheduler/include/scheduler/Fps.h
index d6329e2..19e951a 100644
--- a/services/surfaceflinger/Scheduler/include/scheduler/Fps.h
+++ b/services/surfaceflinger/Scheduler/include/scheduler/Fps.h
@@ -23,6 +23,7 @@
 #include <type_traits>
 
 #include <android-base/stringprintf.h>
+#include <ftl/enum.h>
 #include <scheduler/Time.h>
 
 namespace android {
@@ -81,6 +82,17 @@
     bool valid() const;
 };
 
+// The frame rate category of a Layer.
+enum class FrameRateCategory {
+    Default,
+    NoPreference,
+    Low,
+    Normal,
+    High,
+
+    ftl_last = High
+};
+
 static_assert(std::is_trivially_copyable_v<Fps>);
 
 constexpr Fps operator""_Hz(unsigned long long frequency) {
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index 5c000ed..bc626f3 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -81,7 +81,6 @@
 #include <scheduler/FrameTargeter.h>
 #include <sys/types.h>
 #include <ui/ColorSpace.h>
-#include <ui/DataspaceUtils.h>
 #include <ui/DebugUtils.h>
 #include <ui/DisplayId.h>
 #include <ui/DisplayMode.h>
@@ -89,6 +88,7 @@
 #include <ui/DisplayState.h>
 #include <ui/DynamicDisplayInfo.h>
 #include <ui/GraphicBufferAllocator.h>
+#include <ui/HdrRenderTypeUtils.h>
 #include <ui/LayerStack.h>
 #include <ui/PixelFormat.h>
 #include <ui/StaticDisplayInfo.h>
@@ -134,6 +134,7 @@
 #include "FrontEnd/LayerCreationArgs.h"
 #include "FrontEnd/LayerHandle.h"
 #include "FrontEnd/LayerLifecycleManager.h"
+#include "FrontEnd/LayerLog.h"
 #include "FrontEnd/LayerSnapshot.h"
 #include "HdrLayerInfoReporter.h"
 #include "Layer.h"
@@ -487,7 +488,7 @@
     mIgnoreHdrCameraLayers = ignore_hdr_camera_layers(false);
 
     mLayerLifecycleManagerEnabled =
-            base::GetBoolProperty("persist.debug.sf.enable_layer_lifecycle_manager"s, false);
+            base::GetBoolProperty("persist.debug.sf.enable_layer_lifecycle_manager"s, true);
     mLegacyFrontEndEnabled = !mLayerLifecycleManagerEnabled ||
             base::GetBoolProperty("persist.debug.sf.enable_legacy_frontend"s, false);
 }
@@ -863,30 +864,32 @@
         ALOGE("Run StartPropertySetThread failed!");
     }
 
-    if (mTransactionTracing) {
-        TransactionTraceWriter::getInstance().setWriterFunction([&](const std::string& prefix,
-                                                                    bool overwrite) {
-            auto writeFn = [&]() {
-                const std::string filename =
-                        TransactionTracing::DIR_NAME + prefix + TransactionTracing::FILE_NAME;
-                if (overwrite && access(filename.c_str(), F_OK) == 0) {
-                    ALOGD("TransactionTraceWriter: file=%s already exists", filename.c_str());
-                    return;
-                }
-                mTransactionTracing->flush();
-                mTransactionTracing->writeToFile(filename);
-            };
-            if (std::this_thread::get_id() == mMainThreadId) {
-                writeFn();
-            } else {
-                mScheduler->schedule(writeFn).get();
-            }
-        });
-    }
-
+    initTransactionTraceWriter();
     ALOGV("Done initializing");
 }
 
+void SurfaceFlinger::initTransactionTraceWriter() {
+    if (!mTransactionTracing) {
+        return;
+    }
+    TransactionTraceWriter::getInstance().setWriterFunction(
+            [&](const std::string& filename, bool overwrite) {
+                auto writeFn = [&]() {
+                    if (!overwrite && access(filename.c_str(), F_OK) == 0) {
+                        ALOGD("TransactionTraceWriter: file=%s already exists", filename.c_str());
+                        return;
+                    }
+                    mTransactionTracing->flush();
+                    mTransactionTracing->writeToFile(filename);
+                };
+                if (std::this_thread::get_id() == mMainThreadId) {
+                    writeFn();
+                } else {
+                    mScheduler->schedule(writeFn).get();
+                }
+            });
+}
+
 void SurfaceFlinger::readPersistentProperties() {
     Mutex::Autolock _l(mStateLock);
 
@@ -2290,7 +2293,7 @@
     bool newDataLatched = false;
     if (!mLegacyFrontEndEnabled) {
         ATRACE_NAME("DisplayCallbackAndStatsUpdates");
-        applyTransactions(update.transactions, vsyncId);
+        mustComposite |= applyTransactions(update.transactions, vsyncId);
         traverseLegacyLayers([&](Layer* layer) { layer->commitTransaction(); });
         const nsecs_t latchTime = systemTime();
         bool unused = false;
@@ -2305,11 +2308,22 @@
                 mLegacyLayers[bgColorLayer->sequence] = bgColorLayer;
             }
             const bool willReleaseBufferOnLatch = layer->willReleaseBufferOnLatch();
-            if (!layer->hasReadyFrame() && !willReleaseBufferOnLatch) continue;
 
             auto it = mLegacyLayers.find(layer->id);
             LOG_ALWAYS_FATAL_IF(it == mLegacyLayers.end(), "Couldnt find layer object for %s",
                                 layer->getDebugString().c_str());
+            if (!layer->hasReadyFrame() && !willReleaseBufferOnLatch) {
+                if (!it->second->hasBuffer()) {
+                    // The last latch time is used to classify a missed frame as buffer stuffing
+                    // instead of a missed frame. This is used to identify scenarios where we
+                    // could not latch a buffer or apply a transaction due to backpressure.
+                    // We only update the latch time for buffer less layers here, the latch time
+                    // is updated for buffer layers when the buffer is latched.
+                    it->second->updateLastLatchTime(latchTime);
+                }
+                continue;
+            }
+
             const bool bgColorOnly =
                     !layer->externalTexture && (layer->bgColorLayerId != UNASSIGNED_LAYER_ID);
             if (willReleaseBufferOnLatch) {
@@ -2618,6 +2632,28 @@
     constexpr bool kCursorOnly = false;
     const auto layers = moveSnapshotsToCompositionArgs(refreshArgs, kCursorOnly);
 
+    if (mLayerLifecycleManagerEnabled && !mVisibleRegionsDirty) {
+        for (const auto& [token, display] : FTL_FAKE_GUARD(mStateLock, mDisplays)) {
+            auto compositionDisplay = display->getCompositionDisplay();
+            if (!compositionDisplay->getState().isEnabled) continue;
+            for (auto outputLayer : compositionDisplay->getOutputLayersOrderedByZ()) {
+                if (outputLayer->getLayerFE().getCompositionState() == nullptr) {
+                    // This is unexpected but instead of crashing, capture traces to disk
+                    // and recover gracefully by forcing CE to rebuild layer stack.
+                    ALOGE("Output layer %s for display %s %" PRIu64 " has a null "
+                          "snapshot. Forcing mVisibleRegionsDirty",
+                          outputLayer->getLayerFE().getDebugName(),
+                          compositionDisplay->getName().c_str(), compositionDisplay->getId().value);
+
+                    TransactionTraceWriter::getInstance().invoke(__func__, /* overwrite= */ false);
+                    mVisibleRegionsDirty = true;
+                    refreshArgs.updatingOutputGeometryThisFrame = mVisibleRegionsDirty;
+                    refreshArgs.updatingGeometryThisFrame = mVisibleRegionsDirty;
+                }
+            }
+        }
+    }
+
     mCompositionEngine->present(refreshArgs);
     moveSnapshotsFromCompositionArgs(refreshArgs, layers);
 
@@ -2754,7 +2790,14 @@
             return false;
         }
     }
-    if (isHdrDataspace(snapshot.dataspace)) {
+    // RANGE_EXTENDED layer may identify themselves as being "HDR"
+    // via a desired hdr/sdr ratio
+    auto pixelFormat = snapshot.buffer
+            ? std::make_optional(static_cast<ui::PixelFormat>(snapshot.buffer->getPixelFormat()))
+            : std::nullopt;
+
+    if (getHdrRenderType(snapshot.dataspace, pixelFormat, snapshot.desiredHdrSdrRatio) !=
+        HdrRenderType::SDR) {
         return true;
     }
     // If the layer is not allowed to be dimmed, treat it as HDR. WindowManager may disable
@@ -2764,12 +2807,6 @@
     if (!snapshot.dimmingEnabled) {
         return true;
     }
-    // RANGE_EXTENDED layers may identify themselves as being "HDR" via a desired sdr/hdr ratio
-    if ((snapshot.dataspace & (int32_t)Dataspace::RANGE_MASK) ==
-                (int32_t)Dataspace::RANGE_EXTENDED &&
-        snapshot.desiredHdrSdrRatio > 1.01f) {
-        return true;
-    }
     return false;
 }
 
@@ -3952,6 +3989,9 @@
 
     if (sysprop::use_content_detection_for_refresh_rate(false)) {
         features |= Feature::kContentDetection;
+        if (base::GetBoolProperty("debug.sf.enable_small_dirty_detection"s, false)) {
+            features |= Feature::kSmallDirtyContentDetection;
+        }
     }
     if (base::GetBoolProperty("debug.sf.show_predicted_vsync"s, false)) {
         features |= Feature::kTracePredictedVsync;
@@ -5136,9 +5176,15 @@
         const auto strategy =
             Layer::FrameRate::convertChangeFrameRateStrategy(s.changeFrameRateStrategy);
 
-        if (layer->setFrameRate(
-                Layer::FrameRate(Fps::fromValue(s.frameRate), compatibility, strategy))) {
-          flags |= eTraversalNeeded;
+        if (layer->setFrameRate(Layer::FrameRate::FrameRateVote(Fps::fromValue(s.frameRate),
+                                                                compatibility, strategy))) {
+            flags |= eTraversalNeeded;
+        }
+    }
+    if (what & layer_state_t::eFrameRateCategoryChanged) {
+        const FrameRateCategory category = Layer::FrameRate::convertCategory(s.frameRateCategory);
+        if (layer->setFrameRateCategory(category)) {
+            flags |= eTraversalNeeded;
         }
     }
     if (what & layer_state_t::eFixedTransformHintChanged) {
@@ -5329,6 +5375,11 @@
     if (what & layer_state_t::eDataspaceChanged) {
         if (layer->setDataspace(s.dataspace)) flags |= eTraversalNeeded;
     }
+    if (what & layer_state_t::eExtendedRangeBrightnessChanged) {
+        if (layer->setExtendedRangeBrightness(s.currentHdrSdrRatio, s.desiredHdrSdrRatio)) {
+            flags |= eTraversalNeeded;
+        }
+    }
     if (what & layer_state_t::eBufferChanged) {
         std::optional<ui::Transform::RotationFlags> transformHint = std::nullopt;
         frontend::LayerSnapshot* snapshot = mLayerSnapshotBuilder.getSnapshot(layer->sequence);
@@ -5524,7 +5575,7 @@
 void SurfaceFlinger::onHandleDestroyed(BBinder* handle, sp<Layer>& layer, uint32_t layerId) {
     {
         std::scoped_lock<std::mutex> lock(mCreatedLayersLock);
-        mDestroyedHandles.emplace_back(layerId);
+        mDestroyedHandles.emplace_back(layerId, layer->getDebugName());
     }
 
     mTransactionHandler.onLayerDestroyed(layerId);
@@ -5749,6 +5800,7 @@
                 {"--edid"s, argsDumper(&SurfaceFlinger::dumpRawDisplayIdentificationData)},
                 {"--events"s, dumper(&SurfaceFlinger::dumpEvents)},
                 {"--frametimeline"s, argsDumper(&SurfaceFlinger::dumpFrameTimeline)},
+                {"--hdrinfo"s, dumper(&SurfaceFlinger::dumpHdrInfo)},
                 {"--hwclayers"s, dumper(&SurfaceFlinger::dumpHwcLayersMinidumpLockedLegacy)},
                 {"--latency"s, argsDumper(&SurfaceFlinger::dumpStatsLocked)},
                 {"--latency-clear"s, argsDumper(&SurfaceFlinger::clearStatsLocked)},
@@ -5874,7 +5926,7 @@
 
     const auto name = String8(args[1]);
     mCurrentState.traverseInZOrder([&](Layer* layer) {
-        if (layer->getName() == name.string()) {
+        if (layer->getName() == name.c_str()) {
             layer->dumpFrameStats(result);
         }
     });
@@ -5885,7 +5937,7 @@
     const auto name = clearAll ? String8() : String8(args[1]);
 
     mCurrentState.traverse([&](Layer* layer) {
-        if (clearAll || layer->getName() == name.string()) {
+        if (clearAll || layer->getName() == name.c_str()) {
             layer->clearFrameStats();
         }
     });
@@ -6057,6 +6109,14 @@
     result.append("\n");
 }
 
+void SurfaceFlinger::dumpHdrInfo(std::string& result) const {
+    for (const auto& [displayId, listener] : mHdrLayerInfoListeners) {
+        StringAppendF(&result, "HDR events for display %" PRIu64 "\n", displayId.value);
+        listener->dump(result);
+        result.append("\n");
+    }
+}
+
 LayersProto SurfaceFlinger::dumpDrawingStateProto(uint32_t traceFlags) const {
     std::unordered_set<uint64_t> stackIdsToSkip;
 
@@ -6221,6 +6281,8 @@
     result.append("\nWide-Color information:\n");
     dumpWideColorInfo(result);
 
+    dumpHdrInfo(result);
+
     colorizer.bold(result);
     result.append("Sync configuration: ");
     colorizer.reset(result);
@@ -6529,21 +6591,14 @@
             case 1001:
                 return NAME_NOT_FOUND;
             case 1002: // Toggle flashing on surface damage.
-                if (const int delay = data.readInt32(); delay > 0) {
-                    mDebugFlashDelay = delay;
-                } else {
-                    mDebugFlashDelay = mDebugFlashDelay ? 0 : 1;
-                }
-                scheduleRepaint();
+                sfdo_setDebugFlash(data.readInt32());
                 return NO_ERROR;
             case 1004: // Force composite ahead of next VSYNC.
             case 1006:
-                scheduleComposite(FrameHint::kActive);
+                sfdo_scheduleComposite();
                 return NO_ERROR;
             case 1005: { // Force commit ahead of next VSYNC.
-                Mutex::Autolock lock(mStateLock);
-                setTransactionFlags(eTransactionNeeded | eDisplayTransactionNeeded |
-                                    eTraversalNeeded);
+                sfdo_scheduleCommit();
                 return NO_ERROR;
             }
             case 1007: // Unused.
@@ -6788,19 +6843,13 @@
                 return NO_ERROR;
             }
             case 1034: {
-                auto future = mScheduler->schedule(
-                        [&]() FTL_FAKE_GUARD(mStateLock) FTL_FAKE_GUARD(kMainThreadContext) {
-                            switch (n = data.readInt32()) {
-                                case 0:
-                                case 1:
-                                    enableRefreshRateOverlay(static_cast<bool>(n));
-                                    break;
-                                default:
-                                    reply->writeBool(isRefreshRateOverlayEnabled());
-                            }
-                        });
-
-                future.wait();
+                n = data.readInt32();
+                if (n == 0 || n == 1) {
+                    sfdo_enableRefreshRateOverlay(static_cast<bool>(n));
+                } else {
+                    Mutex::Autolock lock(mStateLock);
+                    reply->writeBool(isRefreshRateOverlayEnabled());
+                }
                 return NO_ERROR;
             }
             case 1035: {
@@ -8151,6 +8200,17 @@
     return NO_ERROR;
 }
 
+status_t SurfaceFlinger::updateSmallAreaDetection(
+        std::vector<std::pair<uid_t, float>>& uidThresholdMappings) {
+    mScheduler->updateSmallAreaDetection(uidThresholdMappings);
+    return NO_ERROR;
+}
+
+status_t SurfaceFlinger::setSmallAreaDetectionThreshold(uid_t uid, float threshold) {
+    mScheduler->setSmallAreaDetectionThreshold(uid, threshold);
+    return NO_ERROR;
+}
+
 void SurfaceFlinger::enableRefreshRateOverlay(bool enable) {
     bool setByHwc = getHwComposer().hasCapability(Capability::REFRESH_RATE_CHANGED_CALLBACK_DEBUG);
     for (const auto& [id, display] : mPhysicalDisplays) {
@@ -8288,6 +8348,15 @@
 void SurfaceFlinger::onActiveDisplaySizeChanged(const DisplayDevice& activeDisplay) {
     mScheduler->onActiveDisplayAreaChanged(activeDisplay.getWidth() * activeDisplay.getHeight());
     getRenderEngine().onActiveDisplaySizeChanged(activeDisplay.getSize());
+
+    // Notify layers to update small dirty flag.
+    if (mScheduler->supportSmallDirtyDetection()) {
+        mCurrentState.traverse([&](Layer* layer) {
+            if (layer->getLayerStack() == activeDisplay.getLayerStack()) {
+                layer->setIsSmallDirty();
+            }
+        });
+    }
 }
 
 sp<DisplayDevice> SurfaceFlinger::getActivatableDisplay() const {
@@ -8425,7 +8494,13 @@
         // Set mirror layer's default layer stack to -1 so it doesn't end up rendered on a display
         // accidentally.
         sp<Layer> rootMirrorLayer = LayerHandle::getLayer(mirrorDisplay.rootHandle);
-        rootMirrorLayer->setLayerStack(ui::LayerStack::fromValue(-1));
+        ssize_t idx = mCurrentState.layersSortedByZ.indexOf(rootMirrorLayer);
+        bool ret = rootMirrorLayer->setLayerStack(ui::LayerStack::fromValue(-1));
+        if (idx >= 0 && ret) {
+            mCurrentState.layersSortedByZ.removeAt(idx);
+            mCurrentState.layersSortedByZ.add(rootMirrorLayer);
+        }
+
         for (const auto& layer : mDrawingState.layersSortedByZ) {
             if (layer->getLayerStack() != mirrorDisplay.layerStack ||
                 layer->isInternalDisplayOverlay()) {
@@ -8728,6 +8803,33 @@
                          std::move(hwcDump), &displays);
 }
 
+// sfdo functions
+
+void SurfaceFlinger::sfdo_enableRefreshRateOverlay(bool active) {
+    auto future = mScheduler->schedule(
+            [&]() FTL_FAKE_GUARD(mStateLock)
+                    FTL_FAKE_GUARD(kMainThreadContext) { enableRefreshRateOverlay(active); });
+    future.wait();
+}
+
+void SurfaceFlinger::sfdo_setDebugFlash(int delay) {
+    if (delay > 0) {
+        mDebugFlashDelay = delay;
+    } else {
+        mDebugFlashDelay = mDebugFlashDelay ? 0 : 1;
+    }
+    scheduleRepaint();
+}
+
+void SurfaceFlinger::sfdo_scheduleComposite() {
+    scheduleComposite(SurfaceFlinger::FrameHint::kActive);
+}
+
+void SurfaceFlinger::sfdo_scheduleCommit() {
+    Mutex::Autolock lock(mStateLock);
+    setTransactionFlags(eTransactionNeeded | eDisplayTransactionNeeded | eTraversalNeeded);
+}
+
 // gui::ISurfaceComposer
 
 binder::Status SurfaceComposerAIDL::bootFinished() {
@@ -9421,6 +9523,60 @@
     return binderStatusFromStatusT(status);
 }
 
+binder::Status SurfaceComposerAIDL::enableRefreshRateOverlay(bool active) {
+    mFlinger->sfdo_enableRefreshRateOverlay(active);
+    return binder::Status::ok();
+}
+
+binder::Status SurfaceComposerAIDL::setDebugFlash(int delay) {
+    mFlinger->sfdo_setDebugFlash(delay);
+    return binder::Status::ok();
+}
+
+binder::Status SurfaceComposerAIDL::scheduleComposite() {
+    mFlinger->sfdo_scheduleComposite();
+    return binder::Status::ok();
+}
+
+binder::Status SurfaceComposerAIDL::scheduleCommit() {
+    mFlinger->sfdo_scheduleCommit();
+    return binder::Status::ok();
+}
+
+binder::Status SurfaceComposerAIDL::updateSmallAreaDetection(const std::vector<int32_t>& uids,
+                                                             const std::vector<float>& thresholds) {
+    status_t status;
+    const int c_uid = IPCThreadState::self()->getCallingUid();
+    if (c_uid == AID_ROOT || c_uid == AID_SYSTEM) {
+        if (uids.size() != thresholds.size()) return binderStatusFromStatusT(BAD_VALUE);
+
+        std::vector<std::pair<uid_t, float>> mappings;
+        const size_t size = uids.size();
+        mappings.reserve(size);
+        for (int i = 0; i < size; i++) {
+            auto row = std::make_pair(static_cast<uid_t>(uids[i]), thresholds[i]);
+            mappings.push_back(row);
+        }
+        status = mFlinger->updateSmallAreaDetection(mappings);
+    } else {
+        ALOGE("updateSmallAreaDetection() permission denied for uid: %d", c_uid);
+        status = PERMISSION_DENIED;
+    }
+    return binderStatusFromStatusT(status);
+}
+
+binder::Status SurfaceComposerAIDL::setSmallAreaDetectionThreshold(int32_t uid, float threshold) {
+    status_t status;
+    const int c_uid = IPCThreadState::self()->getCallingUid();
+    if (c_uid == AID_ROOT || c_uid == AID_SYSTEM) {
+        status = mFlinger->setSmallAreaDetectionThreshold(uid, threshold);
+    } else {
+        ALOGE("setSmallAreaDetectionThreshold() permission denied for uid: %d", c_uid);
+        status = PERMISSION_DENIED;
+    }
+    return binderStatusFromStatusT(status);
+}
+
 binder::Status SurfaceComposerAIDL::getGpuContextPriority(int32_t* outPriority) {
     *outPriority = mFlinger->getGpuContextPriority();
     return binder::Status::ok();
diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h
index 81c3f2f..79dcd0d 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -607,6 +607,10 @@
 
     status_t setOverrideFrameRate(uid_t uid, float frameRate);
 
+    status_t updateSmallAreaDetection(std::vector<std::pair<uid_t, float>>& uidThresholdMappings);
+
+    status_t setSmallAreaDetectionThreshold(uid_t uid, float threshold);
+
     int getGpuContextPriority();
 
     status_t getMaxAcquiredBufferCount(int* buffers) const;
@@ -1086,6 +1090,7 @@
     void dumpDisplayIdentificationData(std::string& result) const REQUIRES(mStateLock);
     void dumpRawDisplayIdentificationData(const DumpArgs&, std::string& result) const;
     void dumpWideColorInfo(std::string& result) const REQUIRES(mStateLock);
+    void dumpHdrInfo(std::string& result) const REQUIRES(mStateLock);
 
     LayersProto dumpDrawingStateProto(uint32_t traceFlags) const;
     void dumpOffscreenLayersProto(LayersProto& layersProto,
@@ -1133,7 +1138,7 @@
     ui::Rotation getPhysicalDisplayOrientation(DisplayId, bool isPrimary) const
             REQUIRES(mStateLock);
     void traverseLegacyLayers(const LayerVector::Visitor& visitor) const;
-
+    void initTransactionTraceWriter();
     sp<StartPropertySetThread> mStartPropertySetThread;
     surfaceflinger::Factory& mFactory;
     pid_t mPid;
@@ -1424,7 +1429,7 @@
     frontend::LayerHierarchyBuilder mLayerHierarchyBuilder{{}};
     frontend::LayerSnapshotBuilder mLayerSnapshotBuilder;
 
-    std::vector<uint32_t> mDestroyedHandles;
+    std::vector<std::pair<uint32_t, std::string>> mDestroyedHandles;
     std::vector<std::unique_ptr<frontend::RequestedLayerState>> mNewLayers;
     std::vector<LayerCreationArgs> mNewLayerArgs;
     // These classes do not store any client state but help with managing transaction callbacks
@@ -1441,6 +1446,11 @@
     // Mirroring
     // Map of displayid to mirrorRoot
     ftl::SmallMap<int64_t, sp<SurfaceControl>, 3> mMirrorMapForDebug;
+
+    void sfdo_enableRefreshRateOverlay(bool active);
+    void sfdo_setDebugFlash(int delay);
+    void sfdo_scheduleComposite();
+    void sfdo_scheduleCommit();
 };
 
 class SurfaceComposerAIDL : public gui::BnSurfaceComposer {
@@ -1547,6 +1557,13 @@
             const sp<IBinder>& displayToken,
             std::optional<gui::DisplayDecorationSupport>* outSupport) override;
     binder::Status setOverrideFrameRate(int32_t uid, float frameRate) override;
+    binder::Status enableRefreshRateOverlay(bool active) override;
+    binder::Status setDebugFlash(int delay) override;
+    binder::Status scheduleComposite() override;
+    binder::Status scheduleCommit() override;
+    binder::Status updateSmallAreaDetection(const std::vector<int32_t>& uids,
+                                            const std::vector<float>& thresholds) override;
+    binder::Status setSmallAreaDetectionThreshold(int32_t uid, float threshold) override;
     binder::Status getGpuContextPriority(int32_t* outPriority) override;
     binder::Status getMaxAcquiredBufferCount(int32_t* buffers) override;
     binder::Status addWindowInfosListener(const sp<gui::IWindowInfosListener>& windowInfosListener,
diff --git a/services/surfaceflinger/Tracing/LayerTracing.cpp b/services/surfaceflinger/Tracing/LayerTracing.cpp
index ecdeabe..b92d50b 100644
--- a/services/surfaceflinger/Tracing/LayerTracing.cpp
+++ b/services/surfaceflinger/Tracing/LayerTracing.cpp
@@ -27,12 +27,13 @@
 #include <utils/Trace.h>
 
 #include "LayerTracing.h"
-#include "RingBuffer.h"
+#include "TransactionRingBuffer.h"
 
 namespace android {
 
 LayerTracing::LayerTracing()
-      : mBuffer(std::make_unique<RingBuffer<LayersTraceFileProto, LayersTraceProto>>()) {}
+      : mBuffer(std::make_unique<TransactionRingBuffer<LayersTraceFileProto, LayersTraceProto>>()) {
+}
 
 LayerTracing::~LayerTracing() = default;
 
diff --git a/services/surfaceflinger/Tracing/LayerTracing.h b/services/surfaceflinger/Tracing/LayerTracing.h
index 40b0fbe..7c0d23d 100644
--- a/services/surfaceflinger/Tracing/LayerTracing.h
+++ b/services/surfaceflinger/Tracing/LayerTracing.h
@@ -30,7 +30,7 @@
 namespace android {
 
 template <typename FileProto, typename EntryProto>
-class RingBuffer;
+class TransactionRingBuffer;
 
 class SurfaceFlinger;
 
@@ -71,7 +71,7 @@
     uint32_t mFlags = TRACE_INPUT;
     mutable std::mutex mTraceLock;
     bool mEnabled GUARDED_BY(mTraceLock) = false;
-    std::unique_ptr<RingBuffer<LayersTraceFileProto, LayersTraceProto>> mBuffer
+    std::unique_ptr<TransactionRingBuffer<LayersTraceFileProto, LayersTraceProto>> mBuffer
             GUARDED_BY(mTraceLock);
     size_t mBufferSizeInBytes GUARDED_BY(mTraceLock) = 20 * 1024 * 1024;
 };
diff --git a/services/surfaceflinger/Tracing/RingBuffer.h b/services/surfaceflinger/Tracing/TransactionRingBuffer.h
similarity index 99%
rename from services/surfaceflinger/Tracing/RingBuffer.h
rename to services/surfaceflinger/Tracing/TransactionRingBuffer.h
index b41c65b..e6f85ca 100644
--- a/services/surfaceflinger/Tracing/RingBuffer.h
+++ b/services/surfaceflinger/Tracing/TransactionRingBuffer.h
@@ -32,7 +32,7 @@
 class SurfaceFlinger;
 
 template <typename FileProto, typename EntryProto>
-class RingBuffer {
+class TransactionRingBuffer {
 public:
     size_t size() const { return mSizeInBytes; }
     size_t used() const { return mUsedInBytes; }
diff --git a/services/surfaceflinger/Tracing/TransactionTracing.cpp b/services/surfaceflinger/Tracing/TransactionTracing.cpp
index 7e330b9..bc69191 100644
--- a/services/surfaceflinger/Tracing/TransactionTracing.cpp
+++ b/services/surfaceflinger/Tracing/TransactionTracing.cpp
@@ -111,7 +111,7 @@
     update.createdLayers = std::move(newUpdate.layerCreationArgs);
     newUpdate.layerCreationArgs.clear();
     update.destroyedLayerHandles.reserve(newUpdate.destroyedHandles.size());
-    for (uint32_t handle : newUpdate.destroyedHandles) {
+    for (auto& [handle, _] : newUpdate.destroyedHandles) {
         update.destroyedLayerHandles.push_back(handle);
     }
     mPendingUpdates.emplace_back(update);
diff --git a/services/surfaceflinger/Tracing/TransactionTracing.h b/services/surfaceflinger/Tracing/TransactionTracing.h
index a59dc6e..422b5f3 100644
--- a/services/surfaceflinger/Tracing/TransactionTracing.h
+++ b/services/surfaceflinger/Tracing/TransactionTracing.h
@@ -30,8 +30,8 @@
 #include "FrontEnd/LayerCreationArgs.h"
 #include "FrontEnd/Update.h"
 #include "LocklessStack.h"
-#include "RingBuffer.h"
 #include "TransactionProtoParser.h"
+#include "TransactionRingBuffer.h"
 
 using namespace android::surfaceflinger;
 
@@ -73,15 +73,19 @@
     static constexpr auto TRACING_VERSION = 1;
 
 private:
+    friend class TransactionTraceWriter;
     friend class TransactionTracingTest;
     friend class SurfaceFlinger;
 
     static constexpr auto DIR_NAME = "/data/misc/wmtrace/";
     static constexpr auto FILE_NAME = "transactions_trace.winscope";
     static constexpr auto FILE_PATH = "/data/misc/wmtrace/transactions_trace.winscope";
+    static std::string getFilePath(const std::string& prefix) {
+        return DIR_NAME + prefix + FILE_NAME;
+    }
 
     mutable std::mutex mTraceLock;
-    RingBuffer<proto::TransactionTraceFile, proto::TransactionTraceEntry> mBuffer
+    TransactionRingBuffer<proto::TransactionTraceFile, proto::TransactionTraceEntry> mBuffer
             GUARDED_BY(mTraceLock);
     size_t mBufferSizeInBytes GUARDED_BY(mTraceLock) = CONTINUOUS_TRACING_BUFFER_SIZE;
     std::unordered_map<uint64_t, proto::TransactionState> mQueuedTransactions
@@ -138,10 +142,16 @@
 
 public:
     void setWriterFunction(
-            std::function<void(const std::string& prefix, bool overwrite)> function) {
+            std::function<void(const std::string& filename, bool overwrite)> function) {
         mWriterFunction = std::move(function);
     }
-    void invoke(const std::string& prefix, bool overwrite) { mWriterFunction(prefix, overwrite); }
+    void invoke(const std::string& prefix, bool overwrite) {
+        mWriterFunction(TransactionTracing::getFilePath(prefix), overwrite);
+    }
+    /* pass in a complete file path for testing */
+    void invokeForTest(const std::string& filename, bool overwrite) {
+        mWriterFunction(filename, overwrite);
+    }
 };
 
 } // namespace android
diff --git a/services/surfaceflinger/Tracing/tools/LayerTraceGenerator.cpp b/services/surfaceflinger/Tracing/tools/LayerTraceGenerator.cpp
index 519ef44..321b8ba 100644
--- a/services/surfaceflinger/Tracing/tools/LayerTraceGenerator.cpp
+++ b/services/surfaceflinger/Tracing/tools/LayerTraceGenerator.cpp
@@ -109,11 +109,11 @@
             ALOGV("       destroyedHandles=%d", entry.destroyed_layers(j));
         }
 
-        std::vector<uint32_t> destroyedHandles;
+        std::vector<std::pair<uint32_t, std::string>> destroyedHandles;
         destroyedHandles.reserve((size_t)entry.destroyed_layer_handles_size());
         for (int j = 0; j < entry.destroyed_layer_handles_size(); j++) {
             ALOGV("       destroyedHandles=%d", entry.destroyed_layer_handles(j));
-            destroyedHandles.push_back(entry.destroyed_layer_handles(j));
+            destroyedHandles.push_back({entry.destroyed_layer_handles(j), ""});
         }
 
         bool displayChanged = entry.displays_changed();
diff --git a/services/surfaceflinger/Utils/RingBuffer.h b/services/surfaceflinger/Utils/RingBuffer.h
new file mode 100644
index 0000000..198e7b2
--- /dev/null
+++ b/services/surfaceflinger/Utils/RingBuffer.h
@@ -0,0 +1,68 @@
+/*
+ * 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 <stddef.h>
+#include <array>
+
+namespace android::utils {
+
+template <class T, size_t SIZE>
+class RingBuffer {
+    RingBuffer(const RingBuffer&) = delete;
+    void operator=(const RingBuffer&) = delete;
+
+public:
+    RingBuffer() = default;
+    ~RingBuffer() = default;
+
+    constexpr size_t capacity() const { return SIZE; }
+
+    size_t size() const { return mCount; }
+
+    T& next() {
+        mHead = static_cast<size_t>(mHead + 1) % SIZE;
+        if (mCount < SIZE) {
+            mCount++;
+        }
+        return mBuffer[static_cast<size_t>(mHead)];
+    }
+
+    T& front() { return (*this)[0]; }
+
+    T& back() { return (*this)[size() - 1]; }
+
+    T& operator[](size_t index) {
+        return mBuffer[(static_cast<size_t>(mHead + 1) + index) % mCount];
+    }
+
+    const T& operator[](size_t index) const {
+        return mBuffer[(static_cast<size_t>(mHead + 1) + index) % mCount];
+    }
+
+    void clear() {
+        mCount = 0;
+        mHead = -1;
+    }
+
+private:
+    std::array<T, SIZE> mBuffer;
+    int mHead = -1;
+    size_t mCount = 0;
+};
+
+} // namespace android::utils
diff --git a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
index ca1af6e..8a050fd 100644
--- a/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
+++ b/services/surfaceflinger/fuzzer/surfaceflinger_fuzzers_utils.h
@@ -295,7 +295,7 @@
 
     // MessageQueue overrides:
     void scheduleFrame() override {}
-    void postMessage(sp<MessageHandler>&&) override {}
+    void postMessage(sp<MessageHandler>&& handler) override { handler->handleMessage(Message()); }
 };
 
 } // namespace scheduler
diff --git a/services/surfaceflinger/tests/EffectLayer_test.cpp b/services/surfaceflinger/tests/EffectLayer_test.cpp
index 52aa502..92ebb8d 100644
--- a/services/surfaceflinger/tests/EffectLayer_test.cpp
+++ b/services/surfaceflinger/tests/EffectLayer_test.cpp
@@ -120,7 +120,7 @@
     const auto canvasSize = 256;
 
     sp<SurfaceControl> leftLayer = createColorLayer("Left", Color::BLUE);
-    sp<SurfaceControl> rightLayer = createColorLayer("Right", Color::GREEN);
+    sp<SurfaceControl> rightLayer = createColorLayer("Right", Color::RED);
     sp<SurfaceControl> blurLayer;
     const auto leftRect = Rect(0, 0, canvasSize / 2, canvasSize);
     const auto rightRect = Rect(canvasSize / 2, 0, canvasSize, canvasSize);
@@ -140,12 +140,12 @@
     {
         auto shot = screenshot();
         shot->expectColor(leftRect, Color::BLUE);
-        shot->expectColor(rightRect, Color::GREEN);
+        shot->expectColor(rightRect, Color::RED);
     }
 
     ASSERT_NO_FATAL_FAILURE(blurLayer = createColorLayer("BackgroundBlur", Color::TRANSPARENT));
 
-    const auto blurRadius = canvasSize / 2;
+    const auto blurRadius = canvasSize / 4;
     asTransaction([&](Transaction& t) {
         t.setLayer(blurLayer, mLayerZBase + 3);
         t.reparent(blurLayer, mParentLayer);
@@ -159,21 +159,26 @@
         auto shot = screenshot();
 
         const auto stepSize = 1;
-        const auto blurAreaOffset = blurRadius * 0.7f;
-        const auto blurAreaStartX = canvasSize / 2 - blurRadius + blurAreaOffset;
-        const auto blurAreaEndX = canvasSize / 2 + blurRadius - blurAreaOffset;
+        const auto expectedBlurAreaSize = blurRadius * 1.5f;
+        const auto blurAreaStartX = canvasSize / 2 - expectedBlurAreaSize / 2;
+        const auto blurAreaEndX = canvasSize / 2 + expectedBlurAreaSize / 2;
+        // testAreaEndY is needed because the setBackgroundBlurRadius API blurs everything behind
+        // the surface, which means it samples pixels from outside the canvasSize and we get some
+        // unexpected colors in the screenshot.
+        const auto testAreaEndY = canvasSize - blurRadius * 2;
+
         Color previousColor;
         Color currentColor;
-        for (int y = 0; y < canvasSize; y++) {
+        for (int y = 0; y < testAreaEndY; y++) {
             shot->checkPixel(0, y, /* r = */ 0, /* g = */ 0, /* b = */ 255);
             previousColor = shot->getPixelColor(0, y);
             for (int x = blurAreaStartX; x < blurAreaEndX; x += stepSize) {
                 currentColor = shot->getPixelColor(x, y);
-                ASSERT_GT(currentColor.g, previousColor.g);
+                ASSERT_GT(currentColor.r, previousColor.r);
                 ASSERT_LT(currentColor.b, previousColor.b);
-                ASSERT_EQ(0, currentColor.r);
+                ASSERT_EQ(0, currentColor.g);
             }
-            shot->checkPixel(canvasSize - 1, y, 0, 255, 0);
+            shot->checkPixel(canvasSize - 1, y, 255, 0, 0);
         }
     }
 }
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index 3dd33b9..8deff85 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -66,6 +66,7 @@
         // option to false temporarily.
         address: true,
     },
+    static_libs: ["libc++fs"],
     srcs: [
         ":libsurfaceflinger_mock_sources",
         ":libsurfaceflinger_sources",
@@ -100,6 +101,7 @@
         "LayerTestUtils.cpp",
         "MessageQueueTest.cpp",
         "PowerAdvisorTest.cpp",
+        "SmallAreaDetectionAllowMappingsTest.cpp",
         "SurfaceFlinger_CreateDisplayTest.cpp",
         "SurfaceFlinger_DestroyDisplayTest.cpp",
         "SurfaceFlinger_DisplayModeSwitching.cpp",
@@ -128,6 +130,7 @@
         "TransactionFrameTracerTest.cpp",
         "TransactionProtoParserTest.cpp",
         "TransactionSurfaceFrameTest.cpp",
+        "TransactionTraceWriterTest.cpp",
         "TransactionTracingTest.cpp",
         "TunnelModeEnabledReporterTest.cpp",
         "StrongTypingTest.cpp",
diff --git a/services/surfaceflinger/tests/unittests/DisplayDevice_InitiateModeChange.cpp b/services/surfaceflinger/tests/unittests/DisplayDevice_InitiateModeChange.cpp
index 60ad7a3..2d87ddd 100644
--- a/services/surfaceflinger/tests/unittests/DisplayDevice_InitiateModeChange.cpp
+++ b/services/surfaceflinger/tests/unittests/DisplayDevice_InitiateModeChange.cpp
@@ -78,7 +78,7 @@
               mDisplay->setDesiredActiveMode(
                       {scheduler::FrameRateMode{90_Hz, kMode90}, Event::None}));
     ASSERT_NE(std::nullopt, mDisplay->getDesiredActiveMode());
-    EXPECT_FRAME_RATE_MODE(kMode90, 90_Hz, mDisplay->getDesiredActiveMode()->modeOpt);
+    EXPECT_FRAME_RATE_MODE(kMode90, 90_Hz, *mDisplay->getDesiredActiveMode()->modeOpt);
     EXPECT_EQ(Event::None, mDisplay->getDesiredActiveMode()->event);
 
     // Setting another mode should be cached but return None
@@ -86,7 +86,7 @@
               mDisplay->setDesiredActiveMode(
                       {scheduler::FrameRateMode{120_Hz, kMode120}, Event::None}));
     ASSERT_NE(std::nullopt, mDisplay->getDesiredActiveMode());
-    EXPECT_FRAME_RATE_MODE(kMode120, 120_Hz, mDisplay->getDesiredActiveMode()->modeOpt);
+    EXPECT_FRAME_RATE_MODE(kMode120, 120_Hz, *mDisplay->getDesiredActiveMode()->modeOpt);
     EXPECT_EQ(Event::None, mDisplay->getDesiredActiveMode()->event);
 }
 
@@ -105,7 +105,7 @@
               mDisplay->setDesiredActiveMode(
                       {scheduler::FrameRateMode{90_Hz, kMode90}, Event::None}));
     ASSERT_NE(std::nullopt, mDisplay->getDesiredActiveMode());
-    EXPECT_FRAME_RATE_MODE(kMode90, 90_Hz, mDisplay->getDesiredActiveMode()->modeOpt);
+    EXPECT_FRAME_RATE_MODE(kMode90, 90_Hz, *mDisplay->getDesiredActiveMode()->modeOpt);
     EXPECT_EQ(Event::None, mDisplay->getDesiredActiveMode()->event);
 
     hal::VsyncPeriodChangeConstraints constraints{
@@ -136,7 +136,7 @@
               mDisplay->setDesiredActiveMode(
                       {scheduler::FrameRateMode{90_Hz, kMode90}, Event::None}));
     ASSERT_NE(std::nullopt, mDisplay->getDesiredActiveMode());
-    EXPECT_FRAME_RATE_MODE(kMode90, 90_Hz, mDisplay->getDesiredActiveMode()->modeOpt);
+    EXPECT_FRAME_RATE_MODE(kMode90, 90_Hz, *mDisplay->getDesiredActiveMode()->modeOpt);
     EXPECT_EQ(Event::None, mDisplay->getDesiredActiveMode()->event);
 
     hal::VsyncPeriodChangeConstraints constraints{
@@ -154,7 +154,7 @@
               mDisplay->setDesiredActiveMode(
                       {scheduler::FrameRateMode{120_Hz, kMode120}, Event::None}));
     ASSERT_NE(std::nullopt, mDisplay->getDesiredActiveMode());
-    EXPECT_FRAME_RATE_MODE(kMode120, 120_Hz, mDisplay->getDesiredActiveMode()->modeOpt);
+    EXPECT_FRAME_RATE_MODE(kMode120, 120_Hz, *mDisplay->getDesiredActiveMode()->modeOpt);
     EXPECT_EQ(Event::None, mDisplay->getDesiredActiveMode()->event);
 
     EXPECT_FRAME_RATE_MODE(kMode90, 90_Hz, *mDisplay->getUpcomingActiveMode().modeOpt);
diff --git a/services/surfaceflinger/tests/unittests/LayerHierarchyTest.h b/services/surfaceflinger/tests/unittests/LayerHierarchyTest.h
index f64ba2a..ff644ba 100644
--- a/services/surfaceflinger/tests/unittests/LayerHierarchyTest.h
+++ b/services/surfaceflinger/tests/unittests/LayerHierarchyTest.h
@@ -170,7 +170,7 @@
         mLifecycleManager.applyTransactions(transactions);
     }
 
-    void destroyLayerHandle(uint32_t id) { mLifecycleManager.onHandlesDestroyed({id}); }
+    void destroyLayerHandle(uint32_t id) { mLifecycleManager.onHandlesDestroyed({{id, "test"}}); }
 
     void updateAndVerify(LayerHierarchyBuilder& hierarchyBuilder) {
         if (mLifecycleManager.getGlobalChanges().test(RequestedLayerState::Changes::Hierarchy)) {
@@ -335,6 +335,17 @@
         mLifecycleManager.applyTransactions(transactions);
     }
 
+    void setFrameRateCategory(uint32_t id, int8_t frameRateCategory) {
+        std::vector<TransactionState> transactions;
+        transactions.emplace_back();
+        transactions.back().states.push_back({});
+
+        transactions.back().states.front().state.what = layer_state_t::eFrameRateCategoryChanged;
+        transactions.back().states.front().layerId = id;
+        transactions.back().states.front().state.frameRateCategory = frameRateCategory;
+        mLifecycleManager.applyTransactions(transactions);
+    }
+
     void setRoundedCorners(uint32_t id, float radius) {
         std::vector<TransactionState> transactions;
         transactions.emplace_back();
diff --git a/services/surfaceflinger/tests/unittests/LayerHistoryTest.cpp b/services/surfaceflinger/tests/unittests/LayerHistoryTest.cpp
index 85d86a7..b67494f 100644
--- a/services/surfaceflinger/tests/unittests/LayerHistoryTest.cpp
+++ b/services/surfaceflinger/tests/unittests/LayerHistoryTest.cpp
@@ -437,6 +437,117 @@
     EXPECT_EQ(0, frequentLayerCount(time));
 }
 
+TEST_F(LayerHistoryTest, oneLayerExplicitCategory) {
+    auto layer = createLayer();
+    EXPECT_CALL(*layer, isVisible()).WillRepeatedly(Return(true));
+    EXPECT_CALL(*layer, getFrameRateForLayerTree())
+            .WillRepeatedly(
+                    Return(Layer::FrameRate(0_Hz, Layer::FrameRateCompatibility::Default,
+                                            Seamlessness::OnlySeamless, FrameRateCategory::High)));
+
+    EXPECT_EQ(1, layerCount());
+    EXPECT_EQ(0, activeLayerCount());
+
+    nsecs_t time = systemTime();
+    for (int i = 0; i < PRESENT_TIME_HISTORY_SIZE; i++) {
+        history().record(layer->getSequence(), layer->getLayerProps(), time, time,
+                         LayerHistory::LayerUpdateType::Buffer);
+        time += HI_FPS_PERIOD;
+    }
+
+    ASSERT_EQ(1, summarizeLayerHistory(time).size());
+    EXPECT_EQ(1, activeLayerCount());
+    EXPECT_EQ(1, frequentLayerCount(time));
+    // First LayerRequirement is the frame rate specification
+    EXPECT_EQ(LayerHistory::LayerVoteType::ExplicitCategory, summarizeLayerHistory(time)[0].vote);
+    EXPECT_EQ(0_Hz, summarizeLayerHistory(time)[0].desiredRefreshRate);
+    EXPECT_EQ(FrameRateCategory::High, summarizeLayerHistory(time)[0].frameRateCategory);
+
+    // layer became inactive, but the vote stays
+    setDefaultLayerVote(layer.get(), LayerHistory::LayerVoteType::Heuristic);
+    time += MAX_ACTIVE_LAYER_PERIOD_NS.count();
+    ASSERT_EQ(1, summarizeLayerHistory(time).size());
+    EXPECT_EQ(1, activeLayerCount());
+    EXPECT_EQ(0, frequentLayerCount(time));
+    EXPECT_EQ(LayerHistory::LayerVoteType::ExplicitCategory, summarizeLayerHistory(time)[0].vote);
+    EXPECT_EQ(0_Hz, summarizeLayerHistory(time)[0].desiredRefreshRate);
+    EXPECT_EQ(FrameRateCategory::High, summarizeLayerHistory(time)[0].frameRateCategory);
+}
+
+// This test case should be the same as oneLayerNoVote except instead of layer vote is NoVote,
+// the category is NoPreference.
+TEST_F(LayerHistoryTest, oneLayerCategoryNoPreference) {
+    auto layer = createLayer();
+    EXPECT_CALL(*layer, isVisible()).WillRepeatedly(Return(true));
+    EXPECT_CALL(*layer, getFrameRateForLayerTree())
+            .WillRepeatedly(Return(Layer::FrameRate(0_Hz, Layer::FrameRateCompatibility::Default,
+                                                    Seamlessness::OnlySeamless,
+                                                    FrameRateCategory::NoPreference)));
+
+    EXPECT_EQ(1, layerCount());
+    EXPECT_EQ(0, activeLayerCount());
+
+    nsecs_t time = systemTime();
+    for (int i = 0; i < PRESENT_TIME_HISTORY_SIZE; i++) {
+        history().record(layer->getSequence(), layer->getLayerProps(), time, time,
+                         LayerHistory::LayerUpdateType::Buffer);
+        time += HI_FPS_PERIOD;
+    }
+
+    ASSERT_TRUE(summarizeLayerHistory(time).empty());
+    EXPECT_EQ(1, activeLayerCount());
+    EXPECT_EQ(1, frequentLayerCount(time));
+
+    // layer became inactive
+    time += MAX_ACTIVE_LAYER_PERIOD_NS.count();
+    ASSERT_TRUE(summarizeLayerHistory(time).empty());
+    EXPECT_EQ(0, activeLayerCount());
+    EXPECT_EQ(0, frequentLayerCount(time));
+}
+
+TEST_F(LayerHistoryTest, oneLayerExplicitVoteWithCategory) {
+    auto layer = createLayer();
+    EXPECT_CALL(*layer, isVisible()).WillRepeatedly(Return(true));
+    EXPECT_CALL(*layer, getFrameRateForLayerTree())
+            .WillRepeatedly(
+                    Return(Layer::FrameRate(73.4_Hz, Layer::FrameRateCompatibility::Default,
+                                            Seamlessness::OnlySeamless, FrameRateCategory::High)));
+
+    EXPECT_EQ(1, layerCount());
+    EXPECT_EQ(0, activeLayerCount());
+
+    nsecs_t time = systemTime();
+    for (int i = 0; i < PRESENT_TIME_HISTORY_SIZE; i++) {
+        history().record(layer->getSequence(), layer->getLayerProps(), time, time,
+                         LayerHistory::LayerUpdateType::Buffer);
+        time += HI_FPS_PERIOD;
+    }
+
+    // There are 2 LayerRequirement's due to the frame rate category.
+    ASSERT_EQ(2, summarizeLayerHistory(time).size());
+    EXPECT_EQ(1, activeLayerCount());
+    EXPECT_EQ(1, frequentLayerCount(time));
+    // First LayerRequirement is the layer's category specification
+    EXPECT_EQ(LayerHistory::LayerVoteType::ExplicitCategory, summarizeLayerHistory(time)[0].vote);
+    EXPECT_EQ(73.4_Hz, summarizeLayerHistory(time)[0].desiredRefreshRate);
+    EXPECT_EQ(FrameRateCategory::High, summarizeLayerHistory(time)[0].frameRateCategory);
+
+    // Second LayerRequirement is the frame rate specification
+    EXPECT_EQ(LayerHistory::LayerVoteType::ExplicitDefault, summarizeLayerHistory(time)[1].vote);
+    EXPECT_EQ(73.4_Hz, summarizeLayerHistory(time)[1].desiredRefreshRate);
+    EXPECT_EQ(FrameRateCategory::High, summarizeLayerHistory(time)[1].frameRateCategory);
+
+    // layer became inactive, but the vote stays
+    setDefaultLayerVote(layer.get(), LayerHistory::LayerVoteType::Heuristic);
+    time += MAX_ACTIVE_LAYER_PERIOD_NS.count();
+    ASSERT_EQ(2, summarizeLayerHistory(time).size());
+    EXPECT_EQ(1, activeLayerCount());
+    EXPECT_EQ(0, frequentLayerCount(time));
+    EXPECT_EQ(LayerHistory::LayerVoteType::ExplicitCategory, summarizeLayerHistory(time)[0].vote);
+    EXPECT_EQ(73.4_Hz, summarizeLayerHistory(time)[0].desiredRefreshRate);
+    EXPECT_EQ(FrameRateCategory::High, summarizeLayerHistory(time)[0].frameRateCategory);
+}
+
 TEST_F(LayerHistoryTest, multipleLayers) {
     auto layer1 = createLayer("A");
     auto layer2 = createLayer("B");
@@ -959,6 +1070,77 @@
     recordFramesAndExpect(layer, time, 27.1_Hz, 30_Hz, PRESENT_TIME_HISTORY_SIZE);
 }
 
+TEST_F(LayerHistoryTest, smallDirtyLayer) {
+    auto layer = createLayer();
+
+    EXPECT_CALL(*layer, isVisible()).WillRepeatedly(Return(true));
+    EXPECT_CALL(*layer, getFrameRateForLayerTree()).WillRepeatedly(Return(Layer::FrameRate()));
+
+    nsecs_t time = systemTime();
+
+    EXPECT_EQ(1, layerCount());
+    EXPECT_EQ(0, activeLayerCount());
+    EXPECT_EQ(0, frequentLayerCount(time));
+
+    LayerHistory::Summary summary;
+
+    // layer is active but infrequent.
+    for (int i = 0; i < PRESENT_TIME_HISTORY_SIZE; i++) {
+        auto props = layer->getLayerProps();
+        if (i % 3 == 0) {
+            props.isSmallDirty = false;
+        } else {
+            props.isSmallDirty = true;
+        }
+
+        history().record(layer->getSequence(), props, time, time,
+                         LayerHistory::LayerUpdateType::Buffer);
+        time += HI_FPS_PERIOD;
+        summary = summarizeLayerHistory(time);
+    }
+
+    ASSERT_EQ(1, summary.size());
+    ASSERT_EQ(LayerHistory::LayerVoteType::Heuristic, summary[0].vote);
+    EXPECT_GE(HI_FPS, summary[0].desiredRefreshRate);
+}
+
+TEST_F(LayerHistoryTest, smallDirtyInMultiLayer) {
+    auto layer1 = createLayer("UI");
+    auto layer2 = createLayer("Video");
+
+    EXPECT_CALL(*layer1, isVisible()).WillRepeatedly(Return(true));
+    EXPECT_CALL(*layer1, getFrameRateForLayerTree()).WillRepeatedly(Return(Layer::FrameRate()));
+
+    EXPECT_CALL(*layer2, isVisible()).WillRepeatedly(Return(true));
+    EXPECT_CALL(*layer2, getFrameRateForLayerTree())
+            .WillRepeatedly(
+                    Return(Layer::FrameRate(30_Hz, Layer::FrameRateCompatibility::Default)));
+
+    nsecs_t time = systemTime();
+
+    EXPECT_EQ(2, layerCount());
+    EXPECT_EQ(0, activeLayerCount());
+    EXPECT_EQ(0, frequentLayerCount(time));
+
+    LayerHistory::Summary summary;
+
+    // layer1 is active but infrequent.
+    for (int i = 0; i < PRESENT_TIME_HISTORY_SIZE; i++) {
+        auto props = layer1->getLayerProps();
+        props.isSmallDirty = true;
+        history().record(layer1->getSequence(), props, 0 /*presentTime*/, time,
+                         LayerHistory::LayerUpdateType::Buffer);
+        history().record(layer2->getSequence(), layer2->getLayerProps(), time, time,
+                         LayerHistory::LayerUpdateType::Buffer);
+        time += HI_FPS_PERIOD;
+        summary = summarizeLayerHistory(time);
+    }
+
+    ASSERT_EQ(1, summary.size());
+    ASSERT_EQ(LayerHistory::LayerVoteType::ExplicitDefault, summary[0].vote);
+    ASSERT_EQ(30_Hz, summary[0].desiredRefreshRate);
+}
+
 class LayerHistoryTestParameterized : public LayerHistoryTest,
                                       public testing::WithParamInterface<std::chrono::nanoseconds> {
 };
diff --git a/services/surfaceflinger/tests/unittests/LayerInfoTest.cpp b/services/surfaceflinger/tests/unittests/LayerInfoTest.cpp
index 5c2d2e1..e0133d6 100644
--- a/services/surfaceflinger/tests/unittests/LayerInfoTest.cpp
+++ b/services/surfaceflinger/tests/unittests/LayerInfoTest.cpp
@@ -24,13 +24,23 @@
 #include "FpsOps.h"
 #include "Scheduler/LayerHistory.h"
 #include "Scheduler/LayerInfo.h"
+#include "TestableScheduler.h"
+#include "TestableSurfaceFlinger.h"
+#include "mock/MockSchedulerCallback.h"
 
 namespace android::scheduler {
 
+using android::mock::createDisplayMode;
+
 class LayerInfoTest : public testing::Test {
 protected:
     using FrameTimeData = LayerInfo::FrameTimeData;
 
+    static constexpr Fps LO_FPS = 30_Hz;
+    static constexpr Fps HI_FPS = 90_Hz;
+
+    LayerInfoTest() { mFlinger.resetScheduler(mScheduler); }
+
     void setFrameTimes(const std::deque<FrameTimeData>& frameTimes) {
         layerInfo.mFrameTimes = frameTimes;
     }
@@ -43,6 +53,16 @@
     auto calculateAverageFrameTime() { return layerInfo.calculateAverageFrameTime(); }
 
     LayerInfo layerInfo{"TestLayerInfo", 0, LayerHistory::LayerVoteType::Heuristic};
+
+    std::shared_ptr<RefreshRateSelector> mSelector =
+            std::make_shared<RefreshRateSelector>(makeModes(createDisplayMode(DisplayModeId(0),
+                                                                              LO_FPS),
+                                                            createDisplayMode(DisplayModeId(1),
+                                                                              HI_FPS)),
+                                                  DisplayModeId(0));
+    mock::SchedulerCallback mSchedulerCallback;
+    TestableScheduler* mScheduler = new TestableScheduler(mSelector, mSchedulerCallback);
+    TestableSurfaceFlinger mFlinger;
 };
 
 namespace {
@@ -171,5 +191,63 @@
     ASSERT_EQ(kExpectedFps, Fps::fromPeriodNsecs(*averageFrameTime));
 }
 
+TEST_F(LayerInfoTest, getRefreshRateVote_explicitVote) {
+    LayerInfo::LayerVote vote = {.type = LayerHistory::LayerVoteType::ExplicitDefault,
+                                 .fps = 20_Hz};
+    layerInfo.setLayerVote(vote);
+
+    auto actualVotes =
+            layerInfo.getRefreshRateVote(*mScheduler->refreshRateSelector(), systemTime());
+    ASSERT_EQ(actualVotes.size(), 1u);
+    ASSERT_EQ(actualVotes[0].type, vote.type);
+    ASSERT_EQ(actualVotes[0].fps, vote.fps);
+    ASSERT_EQ(actualVotes[0].seamlessness, vote.seamlessness);
+    ASSERT_EQ(actualVotes[0].category, vote.category);
+}
+
+TEST_F(LayerInfoTest, getRefreshRateVote_explicitVoteWithCategory) {
+    LayerInfo::LayerVote vote = {.type = LayerHistory::LayerVoteType::ExplicitDefault,
+                                 .fps = 20_Hz,
+                                 .category = FrameRateCategory::High};
+    layerInfo.setLayerVote(vote);
+
+    auto actualVotes =
+            layerInfo.getRefreshRateVote(*mScheduler->refreshRateSelector(), systemTime());
+    ASSERT_EQ(actualVotes.size(), 2u);
+    ASSERT_EQ(actualVotes[0].type, LayerHistory::LayerVoteType::ExplicitCategory);
+    ASSERT_EQ(actualVotes[0].category, vote.category);
+    ASSERT_EQ(actualVotes[1].type, vote.type);
+    ASSERT_EQ(actualVotes[1].fps, vote.fps);
+    ASSERT_EQ(actualVotes[1].seamlessness, vote.seamlessness);
+    ASSERT_EQ(actualVotes[1].category, vote.category);
+}
+
+TEST_F(LayerInfoTest, getRefreshRateVote_explicitCategory) {
+    // When a layer only has a category set, the LayerVoteType should be the LayerInfo's default.
+    // The most common case should be Heuristic.
+    LayerInfo::LayerVote vote = {.type = LayerHistory::LayerVoteType::ExplicitDefault,
+                                 .category = FrameRateCategory::High};
+    layerInfo.setLayerVote(vote);
+
+    auto actualVotes =
+            layerInfo.getRefreshRateVote(*mScheduler->refreshRateSelector(), systemTime());
+    ASSERT_EQ(actualVotes.size(), 1u);
+    ASSERT_EQ(actualVotes[0].type, LayerHistory::LayerVoteType::ExplicitCategory);
+    ASSERT_EQ(actualVotes[0].category, vote.category);
+}
+
+TEST_F(LayerInfoTest, getRefreshRateVote_noData) {
+    LayerInfo::LayerVote vote = {
+            .type = LayerHistory::LayerVoteType::Heuristic,
+    };
+    layerInfo.setLayerVote(vote);
+
+    auto actualVotes =
+            layerInfo.getRefreshRateVote(*mScheduler->refreshRateSelector(), systemTime());
+    ASSERT_EQ(actualVotes.size(), 1u);
+    ASSERT_EQ(actualVotes[0].type, LayerHistory::LayerVoteType::Max);
+    ASSERT_EQ(actualVotes[0].fps, vote.fps);
+}
+
 } // namespace
 } // namespace android::scheduler
diff --git a/services/surfaceflinger/tests/unittests/LayerLifecycleManagerTest.cpp b/services/surfaceflinger/tests/unittests/LayerLifecycleManagerTest.cpp
index 97ef5a2..d65277a 100644
--- a/services/surfaceflinger/tests/unittests/LayerLifecycleManagerTest.cpp
+++ b/services/surfaceflinger/tests/unittests/LayerLifecycleManagerTest.cpp
@@ -84,7 +84,7 @@
     layers.emplace_back(rootLayer(2));
     layers.emplace_back(rootLayer(3));
     lifecycleManager.addLayers(std::move(layers));
-    lifecycleManager.onHandlesDestroyed({1, 2, 3});
+    lifecycleManager.onHandlesDestroyed({{1, "1"}, {2, "2"}, {3, "3"}});
     EXPECT_TRUE(lifecycleManager.getGlobalChanges().test(RequestedLayerState::Changes::Hierarchy));
     lifecycleManager.commitChanges();
     EXPECT_FALSE(lifecycleManager.getGlobalChanges().test(RequestedLayerState::Changes::Hierarchy));
@@ -133,7 +133,7 @@
     layers.emplace_back(rootLayer(1));
     layers.emplace_back(rootLayer(2));
     lifecycleManager.addLayers(std::move(layers));
-    lifecycleManager.onHandlesDestroyed({1});
+    lifecycleManager.onHandlesDestroyed({{1, "1"}});
     lifecycleManager.commitChanges();
 
     SCOPED_TRACE("layerWithoutHandleIsDestroyed");
@@ -149,7 +149,7 @@
     layers.emplace_back(rootLayer(1));
     layers.emplace_back(rootLayer(2));
     lifecycleManager.addLayers(std::move(layers));
-    lifecycleManager.onHandlesDestroyed({1});
+    lifecycleManager.onHandlesDestroyed({{1, "1"}});
     lifecycleManager.commitChanges();
     listener->expectLayersAdded({1, 2});
     listener->expectLayersDestroyed({1});
@@ -173,7 +173,7 @@
     listener->expectLayersAdded({});
     listener->expectLayersDestroyed({});
 
-    lifecycleManager.onHandlesDestroyed({3});
+    lifecycleManager.onHandlesDestroyed({{3, "3"}});
     lifecycleManager.commitChanges();
     listener->expectLayersAdded({});
     listener->expectLayersDestroyed({3});
@@ -194,7 +194,7 @@
     listener->expectLayersDestroyed({});
 
     lifecycleManager.applyTransactions(reparentLayerTransaction(3, UNASSIGNED_LAYER_ID));
-    lifecycleManager.onHandlesDestroyed({3});
+    lifecycleManager.onHandlesDestroyed({{3, "3"}});
     lifecycleManager.commitChanges();
     listener->expectLayersAdded({});
     listener->expectLayersDestroyed({3});
@@ -215,7 +215,7 @@
     listener->expectLayersDestroyed({});
 
     lifecycleManager.applyTransactions(reparentLayerTransaction(3, UNASSIGNED_LAYER_ID));
-    lifecycleManager.onHandlesDestroyed({3, 4});
+    lifecycleManager.onHandlesDestroyed({{3, "3"}, {4, "4"}});
     lifecycleManager.commitChanges();
     listener->expectLayersAdded({});
     listener->expectLayersDestroyed({3, 4});
@@ -376,7 +376,7 @@
     transactions.back().states.front().layerId = 1;
     transactions.emplace_back();
     lifecycleManager.applyTransactions(transactions);
-    lifecycleManager.onHandlesDestroyed({1});
+    lifecycleManager.onHandlesDestroyed({{1, "1"}});
 
     ASSERT_EQ(lifecycleManager.getLayers().size(), 0u);
     ASSERT_EQ(lifecycleManager.getDestroyedLayers().size(), 2u);
@@ -389,4 +389,52 @@
     listener->expectLayersDestroyed({1, bgLayerId});
 }
 
+TEST_F(LayerLifecycleManagerTest, blurSetsVisibilityChangeFlag) {
+    // clear default color on layer so we start with a layer that does not draw anything.
+    setColor(1, {-1.f, -1.f, -1.f});
+    mLifecycleManager.commitChanges();
+
+    // layer has something to draw
+    setBackgroundBlurRadius(1, 2);
+    EXPECT_TRUE(
+            mLifecycleManager.getGlobalChanges().test(RequestedLayerState::Changes::Visibility));
+    mLifecycleManager.commitChanges();
+
+    // layer still has something to draw, so visibility shouldn't change
+    setBackgroundBlurRadius(1, 3);
+    EXPECT_FALSE(
+            mLifecycleManager.getGlobalChanges().test(RequestedLayerState::Changes::Visibility));
+    mLifecycleManager.commitChanges();
+
+    // layer has nothing to draw
+    setBackgroundBlurRadius(1, 0);
+    EXPECT_TRUE(
+            mLifecycleManager.getGlobalChanges().test(RequestedLayerState::Changes::Visibility));
+    mLifecycleManager.commitChanges();
+}
+
+TEST_F(LayerLifecycleManagerTest, colorSetsVisibilityChangeFlag) {
+    // clear default color on layer so we start with a layer that does not draw anything.
+    setColor(1, {-1.f, -1.f, -1.f});
+    mLifecycleManager.commitChanges();
+
+    // layer has something to draw
+    setColor(1, {2.f, 3.f, 4.f});
+    EXPECT_TRUE(
+            mLifecycleManager.getGlobalChanges().test(RequestedLayerState::Changes::Visibility));
+    mLifecycleManager.commitChanges();
+
+    // layer still has something to draw, so visibility shouldn't change
+    setColor(1, {0.f, 0.f, 0.f});
+    EXPECT_FALSE(
+            mLifecycleManager.getGlobalChanges().test(RequestedLayerState::Changes::Visibility));
+    mLifecycleManager.commitChanges();
+
+    // layer has nothing to draw
+    setColor(1, {-1.f, -1.f, -1.f});
+    EXPECT_TRUE(
+            mLifecycleManager.getGlobalChanges().test(RequestedLayerState::Changes::Visibility));
+    mLifecycleManager.commitChanges();
+}
+
 } // namespace android::surfaceflinger::frontend
diff --git a/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp b/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp
index 84c3775..80d913c 100644
--- a/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp
+++ b/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp
@@ -314,13 +314,15 @@
     mLifecycleManager.applyTransactions(transactions);
     UPDATE_AND_VERIFY(mSnapshotBuilder, STARTING_ZORDER);
 
-    EXPECT_EQ(getSnapshot(11)->frameRate.rate.getIntValue(), 90);
-    EXPECT_EQ(getSnapshot(11)->frameRate.type, scheduler::LayerInfo::FrameRateCompatibility::Exact);
-    EXPECT_EQ(getSnapshot(111)->frameRate.rate.getIntValue(), 90);
-    EXPECT_EQ(getSnapshot(111)->frameRate.type,
+    EXPECT_EQ(getSnapshot(11)->frameRate.vote.rate.getIntValue(), 90);
+    EXPECT_EQ(getSnapshot(11)->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Exact);
-    EXPECT_EQ(getSnapshot(1)->frameRate.rate.getIntValue(), 0);
-    EXPECT_EQ(getSnapshot(1)->frameRate.type, scheduler::LayerInfo::FrameRateCompatibility::NoVote);
+    EXPECT_EQ(getSnapshot(111)->frameRate.vote.rate.getIntValue(), 90);
+    EXPECT_EQ(getSnapshot(111)->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::Exact);
+    EXPECT_EQ(getSnapshot(1)->frameRate.vote.rate.getIntValue(), 0);
+    EXPECT_EQ(getSnapshot(1)->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::NoVote);
 }
 
 TEST_F(LayerSnapshotTest, CanCropTouchableRegion) {
@@ -349,16 +351,22 @@
 }
 
 TEST_F(LayerSnapshotTest, blurUpdatesWhenAlphaChanges) {
-    static constexpr int blurRadius = 42;
-    setBackgroundBlurRadius(1221, blurRadius);
+    int blurRadius = 42;
+    setBackgroundBlurRadius(1221, static_cast<uint32_t>(blurRadius));
 
     UPDATE_AND_VERIFY(mSnapshotBuilder, STARTING_ZORDER);
     EXPECT_EQ(getSnapshot({.id = 1221})->backgroundBlurRadius, blurRadius);
 
+    blurRadius = 21;
+    setBackgroundBlurRadius(1221, static_cast<uint32_t>(blurRadius));
+    UPDATE_AND_VERIFY(mSnapshotBuilder, STARTING_ZORDER);
+    EXPECT_EQ(getSnapshot({.id = 1221})->backgroundBlurRadius, blurRadius);
+
     static constexpr float alpha = 0.5;
     setAlpha(12, alpha);
     UPDATE_AND_VERIFY(mSnapshotBuilder, STARTING_ZORDER);
-    EXPECT_EQ(getSnapshot({.id = 1221})->backgroundBlurRadius, blurRadius * alpha);
+    EXPECT_EQ(getSnapshot({.id = 1221})->backgroundBlurRadius,
+              static_cast<int>(static_cast<float>(blurRadius) * alpha));
 }
 
 // Display Mirroring Tests
@@ -525,21 +533,21 @@
 
     UPDATE_AND_VERIFY(mSnapshotBuilder, STARTING_ZORDER);
     // verify parent is gets no vote
-    EXPECT_FALSE(getSnapshot({.id = 1})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 1})->frameRate.type,
+    EXPECT_FALSE(getSnapshot({.id = 1})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 1})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::NoVote);
     EXPECT_TRUE(getSnapshot({.id = 1})->changes.test(RequestedLayerState::Changes::FrameRate));
 
     // verify layer and children get the requested votes
-    EXPECT_TRUE(getSnapshot({.id = 11})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.rate.getValue(), 244.f);
-    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.type,
+    EXPECT_TRUE(getSnapshot({.id = 11})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Default);
     EXPECT_TRUE(getSnapshot({.id = 11})->changes.test(RequestedLayerState::Changes::FrameRate));
 
-    EXPECT_TRUE(getSnapshot({.id = 111})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.rate.getValue(), 244.f);
-    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.type,
+    EXPECT_TRUE(getSnapshot({.id = 111})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Default);
     EXPECT_TRUE(getSnapshot({.id = 111})->changes.test(RequestedLayerState::Changes::FrameRate));
 
@@ -549,24 +557,24 @@
     std::vector<uint32_t> expected = {1, 11, 111, 122, 1221, 12, 121, 13, 2};
     UPDATE_AND_VERIFY(mSnapshotBuilder, expected);
     // verify parent is gets no vote
-    EXPECT_FALSE(getSnapshot({.id = 1})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 1})->frameRate.type,
+    EXPECT_FALSE(getSnapshot({.id = 1})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 1})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::NoVote);
 
     // verify layer and children get the requested votes
-    EXPECT_TRUE(getSnapshot({.id = 11})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.rate.getValue(), 244.f);
-    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.type,
+    EXPECT_TRUE(getSnapshot({.id = 11})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Default);
 
-    EXPECT_TRUE(getSnapshot({.id = 111})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.rate.getValue(), 244.f);
-    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.type,
+    EXPECT_TRUE(getSnapshot({.id = 111})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Default);
 
-    EXPECT_TRUE(getSnapshot({.id = 122})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.rate.getValue(), 244.f);
-    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.type,
+    EXPECT_TRUE(getSnapshot({.id = 122})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Default);
     EXPECT_TRUE(getSnapshot({.id = 122})->changes.test(RequestedLayerState::Changes::FrameRate));
 
@@ -576,31 +584,31 @@
     UPDATE_AND_VERIFY(mSnapshotBuilder, expected);
 
     // verify old parent has invalid framerate (default)
-    EXPECT_FALSE(getSnapshot({.id = 1})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 1})->frameRate.type,
+    EXPECT_FALSE(getSnapshot({.id = 1})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 1})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Default);
     EXPECT_TRUE(getSnapshot({.id = 1})->changes.test(RequestedLayerState::Changes::FrameRate));
 
     // verify new parent get no vote
-    EXPECT_FALSE(getSnapshot({.id = 2})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 2})->frameRate.type,
+    EXPECT_FALSE(getSnapshot({.id = 2})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 2})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::NoVote);
     EXPECT_TRUE(getSnapshot({.id = 2})->changes.test(RequestedLayerState::Changes::FrameRate));
 
     // verify layer and children get the requested votes (unchanged)
-    EXPECT_TRUE(getSnapshot({.id = 11})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.rate.getValue(), 244.f);
-    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.type,
+    EXPECT_TRUE(getSnapshot({.id = 11})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Default);
 
-    EXPECT_TRUE(getSnapshot({.id = 111})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.rate.getValue(), 244.f);
-    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.type,
+    EXPECT_TRUE(getSnapshot({.id = 111})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Default);
 
-    EXPECT_TRUE(getSnapshot({.id = 122})->frameRate.rate.isValid());
-    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.rate.getValue(), 244.f);
-    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.type,
+    EXPECT_TRUE(getSnapshot({.id = 122})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.vote.type,
               scheduler::LayerInfo::FrameRateCompatibility::Default);
 }
 
@@ -610,6 +618,112 @@
     EXPECT_EQ(getSnapshot({.id = 1})->dataspace, ui::Dataspace::V0_SRGB);
 }
 
+// This test is similar to "frameRate" test case but checks that the setFrameRateCategory API
+// interaction also works correctly with the setFrameRate API within SF frontend.
+TEST_F(LayerSnapshotTest, frameRateWithCategory) {
+    // ROOT
+    // ├── 1
+    // │   ├── 11 (frame rate set to 244.f)
+    // │   │   └── 111
+    // │   ├── 12
+    // │   │   ├── 121
+    // │   │   └── 122 (frame rate category set to Normal)
+    // │   │       └── 1221
+    // │   └── 13
+    // └── 2
+    setFrameRate(11, 244.f, 0, 0);
+    setFrameRateCategory(122, 3 /* Normal */);
+
+    UPDATE_AND_VERIFY(mSnapshotBuilder, STARTING_ZORDER);
+    // verify parent 1 gets no vote
+    EXPECT_FALSE(getSnapshot({.id = 1})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 1})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::NoVote);
+    EXPECT_TRUE(getSnapshot({.id = 1})->changes.test(RequestedLayerState::Changes::FrameRate));
+
+    // verify layer 11 and children 111 get the requested votes
+    EXPECT_TRUE(getSnapshot({.id = 11})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::Default);
+    EXPECT_TRUE(getSnapshot({.id = 11})->changes.test(RequestedLayerState::Changes::FrameRate));
+
+    EXPECT_TRUE(getSnapshot({.id = 111})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::Default);
+    EXPECT_TRUE(getSnapshot({.id = 111})->changes.test(RequestedLayerState::Changes::FrameRate));
+
+    // verify parent 12 gets no vote
+    EXPECT_FALSE(getSnapshot({.id = 12})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 12})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::NoVote);
+    EXPECT_TRUE(getSnapshot({.id = 12})->changes.test(RequestedLayerState::Changes::FrameRate));
+
+    // verify layer 122 and children 1221 get the requested votes
+    EXPECT_FALSE(getSnapshot({.id = 122})->frameRate.vote.rate.isValid());
+    EXPECT_TRUE(getSnapshot({.id = 122})->frameRate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::Default);
+    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.category, FrameRateCategory::Normal);
+    EXPECT_TRUE(getSnapshot({.id = 122})->changes.test(RequestedLayerState::Changes::FrameRate));
+
+    EXPECT_FALSE(getSnapshot({.id = 1221})->frameRate.vote.rate.isValid());
+    EXPECT_TRUE(getSnapshot({.id = 1221})->frameRate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 1221})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::Default);
+    EXPECT_EQ(getSnapshot({.id = 1221})->frameRate.category, FrameRateCategory::Normal);
+    EXPECT_TRUE(getSnapshot({.id = 1221})->changes.test(RequestedLayerState::Changes::FrameRate));
+
+    // reparent and verify the child does NOT get the new parent's framerate because it already has
+    // the frame rate category specified.
+    // ROOT
+    //  ├─1
+    //  │  ├─11 (frame rate set to 244.f)
+    //  │  │  ├─111
+    //  │  │  └─122 (frame rate category set to Normal)
+    //  │  │     └─1221
+    //  │  ├─12
+    //  │  │  └─121
+    //  │  └─13
+    //  └─2
+    reparentLayer(122, 11);
+
+    std::vector<uint32_t> expected = {1, 11, 111, 122, 1221, 12, 121, 13, 2};
+    UPDATE_AND_VERIFY(mSnapshotBuilder, expected);
+    // verify parent is gets no vote
+    EXPECT_FALSE(getSnapshot({.id = 1})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 1})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::NoVote);
+
+    // verify layer 11 and children 111 get the requested votes
+    EXPECT_TRUE(getSnapshot({.id = 11})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 11})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::Default);
+
+    EXPECT_TRUE(getSnapshot({.id = 111})->frameRate.vote.rate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.rate.getValue(), 244.f);
+    EXPECT_EQ(getSnapshot({.id = 111})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::Default);
+
+    // verify layer 122 and children 1221 get the requested category vote (unchanged from
+    // reparenting)
+    EXPECT_FALSE(getSnapshot({.id = 122})->frameRate.vote.rate.isValid());
+    EXPECT_TRUE(getSnapshot({.id = 122})->frameRate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::Default);
+    EXPECT_EQ(getSnapshot({.id = 122})->frameRate.category, FrameRateCategory::Normal);
+    EXPECT_TRUE(getSnapshot({.id = 122})->changes.test(RequestedLayerState::Changes::FrameRate));
+
+    EXPECT_FALSE(getSnapshot({.id = 1221})->frameRate.vote.rate.isValid());
+    EXPECT_TRUE(getSnapshot({.id = 1221})->frameRate.isValid());
+    EXPECT_EQ(getSnapshot({.id = 1221})->frameRate.vote.type,
+              scheduler::LayerInfo::FrameRateCompatibility::Default);
+    EXPECT_EQ(getSnapshot({.id = 1221})->frameRate.category, FrameRateCategory::Normal);
+    EXPECT_TRUE(getSnapshot({.id = 1221})->changes.test(RequestedLayerState::Changes::FrameRate));
+}
+
 TEST_F(LayerSnapshotTest, skipRoundCornersWhenProtected) {
     setRoundedCorners(1, 42.f);
     setRoundedCorners(2, 42.f);
diff --git a/services/surfaceflinger/tests/unittests/RefreshRateSelectorTest.cpp b/services/surfaceflinger/tests/unittests/RefreshRateSelectorTest.cpp
index aaf55fb..50c1626 100644
--- a/services/surfaceflinger/tests/unittests/RefreshRateSelectorTest.cpp
+++ b/services/surfaceflinger/tests/unittests/RefreshRateSelectorTest.cpp
@@ -26,6 +26,7 @@
 #include <log/log.h>
 #include <ui/Size.h>
 
+#include <scheduler/Fps.h>
 #include <scheduler/FrameRateMode.h>
 #include "DisplayHardware/HWC2.h"
 #include "FpsOps.h"
@@ -1381,6 +1382,120 @@
     EXPECT_FALSE(signals.touch);
 }
 
+TEST_P(RefreshRateSelectorTest, getBestFrameRateMode_withFrameRateCategory_30_60_90_120) {
+    auto selector = createSelector(makeModes(kMode30, kMode60, kMode90, kMode120), kModeId60);
+
+    std::vector<LayerRequirement> layers = {{.vote = LayerVoteType::ExplicitDefault, .weight = 1.f},
+                                            {.vote = LayerVoteType::ExplicitCategory,
+                                             .weight = 1.f}};
+    auto& lr = layers[0];
+
+    struct Case {
+        // Params
+        Fps desiredFrameRate = 0_Hz;
+        FrameRateCategory frameRateCategory = FrameRateCategory::Default;
+
+        // Expected result
+        Fps expectedFrameRate = 0_Hz;
+    };
+
+    // Prepare a table with the vote and the expected refresh rate
+    const std::initializer_list<Case> testCases = {
+            // Cases that only have frame rate category requirements, but no desired frame rate.
+            // When frame rates get an equal score, the lower is chosen, unless there are Max votes.
+            {0_Hz, FrameRateCategory::High, 90_Hz},
+            {0_Hz, FrameRateCategory::Normal, 60_Hz},
+            {0_Hz, FrameRateCategory::Low, 30_Hz},
+            {0_Hz, FrameRateCategory::NoPreference, 60_Hz},
+
+            // Cases that have both desired frame rate and frame rate category requirements.
+            {24_Hz, FrameRateCategory::High, 120_Hz},
+            {30_Hz, FrameRateCategory::High, 90_Hz},
+            {12_Hz, FrameRateCategory::Normal, 60_Hz},
+            {30_Hz, FrameRateCategory::NoPreference, 30_Hz},
+
+            // Cases that only have desired frame rate.
+            {30_Hz, FrameRateCategory::Default, 30_Hz},
+    };
+
+    for (auto testCase : testCases) {
+        ALOGI("**** %s: Testing desiredFrameRate=%s, frameRateCategory=%s", __func__,
+              to_string(testCase.desiredFrameRate).c_str(),
+              ftl::enum_string(testCase.frameRateCategory).c_str());
+
+        lr.desiredRefreshRate = testCase.desiredFrameRate;
+
+        std::stringstream ss;
+        ss << to_string(testCase.desiredFrameRate)
+           << ", category=" << ftl::enum_string(testCase.frameRateCategory);
+        lr.name = ss.str();
+
+        if (testCase.frameRateCategory != FrameRateCategory::Default) {
+            layers[1].frameRateCategory = testCase.frameRateCategory;
+        }
+
+        EXPECT_EQ(testCase.expectedFrameRate, selector.getBestFrameRateMode(layers)->getFps())
+                << "did not get expected frame rate for " << lr.name;
+    }
+}
+
+TEST_P(RefreshRateSelectorTest, getBestFrameRateMode_withFrameRateCategory_60_120) {
+    auto selector = createSelector(makeModes(kMode60, kMode120), kModeId60);
+
+    std::vector<LayerRequirement> layers = {{.vote = LayerVoteType::ExplicitDefault, .weight = 1.f},
+                                            {.vote = LayerVoteType::ExplicitCategory,
+                                             .weight = 1.f}};
+    auto& lr = layers[0];
+
+    struct Case {
+        // Params
+        Fps desiredFrameRate = 0_Hz;
+        FrameRateCategory frameRateCategory = FrameRateCategory::Default;
+
+        // Expected result
+        Fps expectedFrameRate = 0_Hz;
+    };
+
+    // Prepare a table with the vote and the expected refresh rate
+    const std::initializer_list<Case> testCases = {
+            // Cases that only have frame rate category requirements, but no desired frame rate.
+            // When frame rates get an equal score, the lower is chosen, unless there are Max votes.
+            {0_Hz, FrameRateCategory::High, 120_Hz},
+            {0_Hz, FrameRateCategory::Normal, 60_Hz},
+            {0_Hz, FrameRateCategory::Low, 60_Hz},
+            {0_Hz, FrameRateCategory::NoPreference, 60_Hz},
+
+            // Cases that have both desired frame rate and frame rate category requirements.
+            {24_Hz, FrameRateCategory::High, 120_Hz},
+            {30_Hz, FrameRateCategory::High, 120_Hz},
+            {12_Hz, FrameRateCategory::Normal, 60_Hz},
+            {30_Hz, FrameRateCategory::NoPreference, 60_Hz},
+
+            // Cases that only have desired frame rate.
+            {30_Hz, FrameRateCategory::Default, 60_Hz},
+    };
+
+    for (auto testCase : testCases) {
+        ALOGI("**** %s: Testing desiredFrameRate=%s, frameRateCategory=%s", __func__,
+              to_string(testCase.desiredFrameRate).c_str(),
+              ftl::enum_string(testCase.frameRateCategory).c_str());
+
+        lr.desiredRefreshRate = testCase.desiredFrameRate;
+
+        std::stringstream ss;
+        ss << to_string(testCase.desiredFrameRate)
+           << ", category=" << ftl::enum_string(testCase.frameRateCategory);
+        lr.name = ss.str();
+
+        if (testCase.frameRateCategory != FrameRateCategory::Default) {
+            layers[1].frameRateCategory = testCase.frameRateCategory;
+        }
+
+        EXPECT_EQ(testCase.expectedFrameRate, selector.getBestFrameRateMode(layers)->getFps())
+                << "did not get expected frame rate for " << lr.name;
+    }
+}
+
 TEST_P(RefreshRateSelectorTest, getBestFrameRateMode_ExplicitDefault) {
     auto selector = createSelector(kModes_60_90_72_120, kModeId60);
 
@@ -3125,5 +3240,69 @@
                       {DisplayModeId(kModeId60), kLowerThanMin, kLowerThanMin}));
 }
 
+// b/296079213
+TEST_P(RefreshRateSelectorTest, frameRateOverrideInBlockingZone60_120) {
+    auto selector = createSelector(kModes_60_120, kModeId120);
+
+    const FpsRange only120 = {120_Hz, 120_Hz};
+    const FpsRange allRange = {0_Hz, 120_Hz};
+    EXPECT_EQ(SetPolicyResult::Changed,
+              selector.setDisplayManagerPolicy(
+                      {kModeId120, {only120, allRange}, {allRange, allRange}}));
+
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "30Hz ExplicitExactOrMultiple";
+    layers[0].desiredRefreshRate = 30_Hz;
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+
+    if (GetParam() != Config::FrameRateOverride::Enabled) {
+        EXPECT_FRAME_RATE_MODE(kMode120, 120_Hz,
+                               selector.getBestScoredFrameRate(layers).frameRateMode);
+    } else {
+        EXPECT_FRAME_RATE_MODE(kMode120, 30_Hz,
+                               selector.getBestScoredFrameRate(layers).frameRateMode);
+    }
+}
+
+TEST_P(RefreshRateSelectorTest, frameRateOverrideInBlockingZone60_90) {
+    auto selector = createSelector(kModes_60_90, kModeId90);
+
+    const FpsRange only90 = {90_Hz, 90_Hz};
+    const FpsRange allRange = {0_Hz, 90_Hz};
+    EXPECT_EQ(SetPolicyResult::Changed,
+              selector.setDisplayManagerPolicy(
+                      {kModeId90, {only90, allRange}, {allRange, allRange}}));
+
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "30Hz ExplicitExactOrMultiple";
+    layers[0].desiredRefreshRate = 30_Hz;
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+
+    if (GetParam() != Config::FrameRateOverride::Enabled) {
+        EXPECT_FRAME_RATE_MODE(kMode90, 90_Hz,
+                               selector.getBestScoredFrameRate(layers).frameRateMode);
+    } else {
+        EXPECT_FRAME_RATE_MODE(kMode90, 30_Hz,
+                               selector.getBestScoredFrameRate(layers).frameRateMode);
+    }
+}
+
+TEST_P(RefreshRateSelectorTest, frameRateOverrideInBlockingZone60_90_NonDivisor) {
+    auto selector = createSelector(kModes_60_90, kModeId90);
+
+    const FpsRange only90 = {90_Hz, 90_Hz};
+    const FpsRange allRange = {0_Hz, 90_Hz};
+    EXPECT_EQ(SetPolicyResult::Changed,
+              selector.setDisplayManagerPolicy(
+                      {kModeId90, {only90, allRange}, {allRange, allRange}}));
+
+    std::vector<LayerRequirement> layers = {{.weight = 1.f}};
+    layers[0].name = "60Hz ExplicitExactOrMultiple";
+    layers[0].desiredRefreshRate = 60_Hz;
+    layers[0].vote = LayerVoteType::ExplicitExactOrMultiple;
+
+    EXPECT_FRAME_RATE_MODE(kMode90, 90_Hz, selector.getBestScoredFrameRate(layers).frameRateMode);
+}
+
 } // namespace
 } // namespace android::scheduler
diff --git a/services/surfaceflinger/tests/unittests/SetFrameRateTest.cpp b/services/surfaceflinger/tests/unittests/SetFrameRateTest.cpp
index a1e4e25..608fa76 100644
--- a/services/surfaceflinger/tests/unittests/SetFrameRateTest.cpp
+++ b/services/surfaceflinger/tests/unittests/SetFrameRateTest.cpp
@@ -97,7 +97,7 @@
     const auto& layerFactory = GetParam();
 
     auto layer = mLayers.emplace_back(layerFactory->createLayer(mFlinger));
-    layer->setFrameRate(FRAME_RATE_VOTE1);
+    layer->setFrameRate(FRAME_RATE_VOTE1.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_VOTE1, layer->getFrameRateForLayerTree());
 }
@@ -114,13 +114,13 @@
     addChild(parent, child1);
     addChild(child1, child2);
 
-    child2->setFrameRate(FRAME_RATE_VOTE1);
+    child2->setFrameRate(FRAME_RATE_VOTE1.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_TREE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_TREE, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child2->getFrameRateForLayerTree());
 
-    child2->setFrameRate(FRAME_RATE_NO_VOTE);
+    child2->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_NO_VOTE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child1->getFrameRateForLayerTree());
@@ -139,27 +139,27 @@
     addChild(parent, child1);
     addChild(child1, child2);
 
-    child2->setFrameRate(FRAME_RATE_VOTE1);
-    child1->setFrameRate(FRAME_RATE_VOTE2);
-    parent->setFrameRate(FRAME_RATE_VOTE3);
+    child2->setFrameRate(FRAME_RATE_VOTE1.vote);
+    child1->setFrameRate(FRAME_RATE_VOTE2.vote);
+    parent->setFrameRate(FRAME_RATE_VOTE3.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_VOTE3, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE2, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child2->getFrameRateForLayerTree());
 
-    child2->setFrameRate(FRAME_RATE_NO_VOTE);
+    child2->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_VOTE3, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE2, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE2, child2->getFrameRateForLayerTree());
 
-    child1->setFrameRate(FRAME_RATE_NO_VOTE);
+    child1->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_VOTE3, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE3, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE3, child2->getFrameRateForLayerTree());
 
-    parent->setFrameRate(FRAME_RATE_NO_VOTE);
+    parent->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_NO_VOTE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child1->getFrameRateForLayerTree());
@@ -178,13 +178,13 @@
     addChild(parent, child1);
     addChild(child1, child2);
 
-    parent->setFrameRate(FRAME_RATE_VOTE1);
+    parent->setFrameRate(FRAME_RATE_VOTE1.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_VOTE1, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child2->getFrameRateForLayerTree());
 
-    parent->setFrameRate(FRAME_RATE_NO_VOTE);
+    parent->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_NO_VOTE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child1->getFrameRateForLayerTree());
@@ -203,27 +203,27 @@
     addChild(parent, child1);
     addChild(child1, child2);
 
-    child2->setFrameRate(FRAME_RATE_VOTE1);
-    child1->setFrameRate(FRAME_RATE_VOTE2);
-    parent->setFrameRate(FRAME_RATE_VOTE3);
+    child2->setFrameRate(FRAME_RATE_VOTE1.vote);
+    child1->setFrameRate(FRAME_RATE_VOTE2.vote);
+    parent->setFrameRate(FRAME_RATE_VOTE3.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_VOTE3, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE2, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child2->getFrameRateForLayerTree());
 
-    parent->setFrameRate(FRAME_RATE_NO_VOTE);
+    parent->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_TREE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE2, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child2->getFrameRateForLayerTree());
 
-    child1->setFrameRate(FRAME_RATE_NO_VOTE);
+    child1->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_TREE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_TREE, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child2->getFrameRateForLayerTree());
 
-    child2->setFrameRate(FRAME_RATE_NO_VOTE);
+    child2->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_NO_VOTE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child1->getFrameRateForLayerTree());
@@ -241,7 +241,7 @@
 
     addChild(parent, child1);
 
-    parent->setFrameRate(FRAME_RATE_VOTE1);
+    parent->setFrameRate(FRAME_RATE_VOTE1.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_VOTE1, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child1->getFrameRateForLayerTree());
@@ -253,7 +253,7 @@
     EXPECT_EQ(FRAME_RATE_VOTE1, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child2->getFrameRateForLayerTree());
 
-    parent->setFrameRate(FRAME_RATE_NO_VOTE);
+    parent->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_NO_VOTE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child1->getFrameRateForLayerTree());
@@ -272,7 +272,7 @@
     addChild(parent, child1);
     addChild(child1, child2);
 
-    parent->setFrameRate(FRAME_RATE_VOTE1);
+    parent->setFrameRate(FRAME_RATE_VOTE1.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_VOTE1, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child1->getFrameRateForLayerTree());
@@ -284,7 +284,7 @@
     EXPECT_EQ(FRAME_RATE_VOTE1, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child2->getFrameRateForLayerTree());
 
-    parent->setFrameRate(FRAME_RATE_NO_VOTE);
+    parent->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_NO_VOTE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child1->getFrameRateForLayerTree());
@@ -305,14 +305,14 @@
     addChild(child1, child2);
     addChild(child1, child2_1);
 
-    child2->setFrameRate(FRAME_RATE_VOTE1);
+    child2->setFrameRate(FRAME_RATE_VOTE1.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_TREE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_TREE, child1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_VOTE1, child2->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child2_1->getFrameRateForLayerTree());
 
-    child2->setFrameRate(FRAME_RATE_NO_VOTE);
+    child2->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_NO_VOTE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child1->getFrameRateForLayerTree());
@@ -375,7 +375,7 @@
     auto child = mLayers.emplace_back(layerFactory->createLayer(mFlinger));
     addChild(parent, child);
 
-    parent->setFrameRate(FRAME_RATE_VOTE1);
+    parent->setFrameRate(FRAME_RATE_VOTE1.vote);
     commitTransaction();
 
     auto& history = mFlinger.mutableScheduler().mutableLayerHistory();
@@ -388,8 +388,8 @@
     const auto summary = history.summarize(*selectorPtr, 0);
 
     ASSERT_EQ(2u, summary.size());
-    EXPECT_EQ(FRAME_RATE_VOTE1.rate, summary[0].desiredRefreshRate);
-    EXPECT_EQ(FRAME_RATE_VOTE1.rate, summary[1].desiredRefreshRate);
+    EXPECT_EQ(FRAME_RATE_VOTE1.vote.rate, summary[0].desiredRefreshRate);
+    EXPECT_EQ(FRAME_RATE_VOTE1.vote.rate, summary[1].desiredRefreshRate);
 }
 
 TEST_P(SetFrameRateTest, addChildForParentWithTreeVote) {
@@ -405,7 +405,7 @@
     addChild(parent, child1);
     addChild(child1, childOfChild1);
 
-    childOfChild1->setFrameRate(FRAME_RATE_VOTE1);
+    childOfChild1->setFrameRate(FRAME_RATE_VOTE1.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_TREE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_TREE, child1->getFrameRateForLayerTree());
@@ -419,7 +419,7 @@
     EXPECT_EQ(FRAME_RATE_VOTE1, childOfChild1->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child2->getFrameRateForLayerTree());
 
-    childOfChild1->setFrameRate(FRAME_RATE_NO_VOTE);
+    childOfChild1->setFrameRate(FRAME_RATE_NO_VOTE.vote);
     commitTransaction();
     EXPECT_EQ(FRAME_RATE_NO_VOTE, parent->getFrameRateForLayerTree());
     EXPECT_EQ(FRAME_RATE_NO_VOTE, child1->getFrameRateForLayerTree());
diff --git a/services/surfaceflinger/tests/unittests/SmallAreaDetectionAllowMappingsTest.cpp b/services/surfaceflinger/tests/unittests/SmallAreaDetectionAllowMappingsTest.cpp
new file mode 100644
index 0000000..b910485
--- /dev/null
+++ b/services/surfaceflinger/tests/unittests/SmallAreaDetectionAllowMappingsTest.cpp
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "SmallAreaDetectionAllowMappingsTest"
+
+#include <gtest/gtest.h>
+
+#include "Scheduler/SmallAreaDetectionAllowMappings.h"
+
+namespace android::scheduler {
+
+class SmallAreaDetectionMappingsAllowTest : public testing::Test {
+protected:
+    SmallAreaDetectionAllowMappings mMappings;
+};
+
+namespace {
+TEST_F(SmallAreaDetectionMappingsAllowTest, testUpdate) {
+    const uid_t uid1 = 10100;
+    const uid_t uid2 = 10101;
+    const float threshold1 = 0.05f;
+    const float threshold2 = 0.07f;
+    std::vector<std::pair<uid_t, float>> mappings;
+    mappings.reserve(2);
+    mappings.push_back(std::make_pair(uid1, threshold1));
+    mappings.push_back(std::make_pair(uid2, threshold2));
+
+    mMappings.update(mappings);
+    ASSERT_EQ(mMappings.getThresholdForUid(uid1).value(), threshold1);
+    ASSERT_EQ(mMappings.getThresholdForUid(uid2).value(), threshold2);
+}
+
+TEST_F(SmallAreaDetectionMappingsAllowTest, testSetThesholdForUid) {
+    const uid_t uid = 10111;
+    const float threshold = 0.05f;
+
+    mMappings.setThesholdForUid(uid, threshold);
+    ASSERT_EQ(mMappings.getThresholdForUid(uid), threshold);
+}
+
+TEST_F(SmallAreaDetectionMappingsAllowTest, testUidNotInTheMappings) {
+    const uid_t uid = 10222;
+    ASSERT_EQ(mMappings.getThresholdForUid(uid), std::nullopt);
+}
+
+} // namespace
+} // namespace android::scheduler
diff --git a/services/surfaceflinger/tests/unittests/SurfaceFlinger_CreateDisplayTest.cpp b/services/surfaceflinger/tests/unittests/SurfaceFlinger_CreateDisplayTest.cpp
index 1cc9ba4..dbf0cd8 100644
--- a/services/surfaceflinger/tests/unittests/SurfaceFlinger_CreateDisplayTest.cpp
+++ b/services/surfaceflinger/tests/unittests/SurfaceFlinger_CreateDisplayTest.cpp
@@ -91,7 +91,7 @@
     const auto& display = getCurrentDisplayState(displayToken);
     EXPECT_TRUE(display.isVirtual());
     EXPECT_FALSE(display.isSecure);
-    EXPECT_EQ(name.string(), display.displayName);
+    EXPECT_EQ(name.c_str(), display.displayName);
 
     // --------------------------------------------------------------------
     // Cleanup conditions
@@ -123,7 +123,7 @@
     const auto& display = getCurrentDisplayState(displayToken);
     EXPECT_TRUE(display.isVirtual());
     EXPECT_TRUE(display.isSecure);
-    EXPECT_EQ(name.string(), display.displayName);
+    EXPECT_EQ(name.c_str(), display.displayName);
 
     // --------------------------------------------------------------------
     // Cleanup conditions
diff --git a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
index 02fa415..8458aa3 100644
--- a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
+++ b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
@@ -651,6 +651,11 @@
 
     auto fromHandle(const sp<IBinder>& handle) { return LayerHandle::getLayer(handle); }
 
+    auto initTransactionTraceWriter() {
+        mFlinger->mTransactionTracing.emplace();
+        return mFlinger->initTransactionTraceWriter();
+    }
+
     ~TestableSurfaceFlinger() {
         // All these pointer and container clears help ensure that GMock does
         // not report a leaked object, since the SurfaceFlinger instance may
@@ -664,6 +669,7 @@
         mFlinger->mCompositionEngine->setHwComposer(std::unique_ptr<HWComposer>());
         mFlinger->mRenderEngine = std::unique_ptr<renderengine::RenderEngine>();
         mFlinger->mCompositionEngine->setRenderEngine(mFlinger->mRenderEngine.get());
+        mFlinger->mTransactionTracing.reset();
     }
 
     /* ------------------------------------------------------------------------
diff --git a/services/surfaceflinger/tests/unittests/TransactionTraceWriterTest.cpp b/services/surfaceflinger/tests/unittests/TransactionTraceWriterTest.cpp
new file mode 100644
index 0000000..379135e
--- /dev/null
+++ b/services/surfaceflinger/tests/unittests/TransactionTraceWriterTest.cpp
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#undef LOG_TAG
+#define LOG_TAG "TransactionTraceWriterTest"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <log/log.h>
+#include <filesystem>
+
+#include "TestableSurfaceFlinger.h"
+
+namespace android {
+
+class TransactionTraceWriterTest : public testing::Test {
+protected:
+    std::string mFilename = "/data/local/tmp/testfile_transaction_trace.winscope";
+
+    void SetUp() { mFlinger.initTransactionTraceWriter(); }
+    void TearDown() { std::filesystem::remove(mFilename); }
+
+    void verifyTraceFile() {
+        std::fstream file(mFilename, std::ios::in);
+        ASSERT_TRUE(file.is_open());
+        std::string line;
+        char magicNumber[8];
+        file.read(magicNumber, 8);
+        EXPECT_EQ("\tTNXTRAC", std::string(magicNumber, magicNumber + 8));
+    }
+
+    TestableSurfaceFlinger mFlinger;
+};
+
+TEST_F(TransactionTraceWriterTest, canWriteToFile) {
+    TransactionTraceWriter::getInstance().invokeForTest(mFilename, /* overwrite */ true);
+    EXPECT_EQ(access(mFilename.c_str(), F_OK), 0);
+    verifyTraceFile();
+}
+
+TEST_F(TransactionTraceWriterTest, canOverwriteFile) {
+    std::string testLine = "test";
+    {
+        std::ofstream file(mFilename, std::ios::out);
+        file << testLine;
+    }
+    TransactionTraceWriter::getInstance().invokeForTest(mFilename, /* overwrite */ true);
+    verifyTraceFile();
+}
+
+TEST_F(TransactionTraceWriterTest, doNotOverwriteFile) {
+    std::string testLine = "test";
+    {
+        std::ofstream file(mFilename, std::ios::out);
+        file << testLine;
+    }
+    TransactionTraceWriter::getInstance().invokeForTest(mFilename, /* overwrite */ false);
+    {
+        std::fstream file(mFilename, std::ios::in);
+        ASSERT_TRUE(file.is_open());
+        std::string line;
+        std::getline(file, line);
+        EXPECT_EQ(line, testLine);
+    }
+}
+} // namespace android
\ No newline at end of file
diff --git a/services/surfaceflinger/tests/unittests/TransactionTracingTest.cpp b/services/surfaceflinger/tests/unittests/TransactionTracingTest.cpp
index 809966f..71a2d2b 100644
--- a/services/surfaceflinger/tests/unittests/TransactionTracingTest.cpp
+++ b/services/surfaceflinger/tests/unittests/TransactionTracingTest.cpp
@@ -25,7 +25,6 @@
 #include "FrontEnd/LayerCreationArgs.h"
 #include "FrontEnd/Update.h"
 #include "Tracing/LayerTracing.h"
-#include "Tracing/RingBuffer.h"
 #include "Tracing/TransactionTracing.h"
 
 using namespace android::surfaceflinger;
diff --git a/services/surfaceflinger/tests/unittests/mock/MockFrameRateMode.h b/services/surfaceflinger/tests/unittests/mock/MockFrameRateMode.h
index ef9cd9b..4cfdd58 100644
--- a/services/surfaceflinger/tests/unittests/mock/MockFrameRateMode.h
+++ b/services/surfaceflinger/tests/unittests/mock/MockFrameRateMode.h
@@ -19,5 +19,7 @@
 #include <scheduler/FrameRateMode.h>
 
 // Use a C style macro to keep the line numbers printed in gtest
-#define EXPECT_FRAME_RATE_MODE(modePtr, fps, mode) \
-    EXPECT_EQ((scheduler::FrameRateMode{(fps), (modePtr)}), (mode))
+#define EXPECT_FRAME_RATE_MODE(_modePtr, _fps, _mode)                                \
+    EXPECT_EQ((scheduler::FrameRateMode{(_fps), (_modePtr)}), (_mode))               \
+            << "Expected " << (_fps) << " (" << (_modePtr)->getFps() << ") but was " \
+            << (_mode).fps << " (" << (_mode).modePtr->getFps() << ")"
diff --git a/services/surfaceflinger/tests/utils/ScreenshotUtils.h b/services/surfaceflinger/tests/utils/ScreenshotUtils.h
index f297da5..1675584 100644
--- a/services/surfaceflinger/tests/utils/ScreenshotUtils.h
+++ b/services/surfaceflinger/tests/utils/ScreenshotUtils.h
@@ -170,7 +170,7 @@
             String8 err(String8::format("pixel @ (%3d, %3d): "
                                         "expected [%3d, %3d, %3d], got [%3d, %3d, %3d]",
                                         x, y, r, g, b, pixel[0], pixel[1], pixel[2]));
-            EXPECT_EQ(String8(), err) << err.string();
+            EXPECT_EQ(String8(), err) << err.c_str();
         }
     }
 
diff --git a/services/vibratorservice/VibratorCallbackScheduler.cpp b/services/vibratorservice/VibratorCallbackScheduler.cpp
index f2870b0..7eda9ef 100644
--- a/services/vibratorservice/VibratorCallbackScheduler.cpp
+++ b/services/vibratorservice/VibratorCallbackScheduler.cpp
@@ -29,8 +29,11 @@
     return mExpiration <= std::chrono::steady_clock::now();
 }
 
-DelayedCallback::Timestamp DelayedCallback::getExpiration() const {
-    return mExpiration;
+std::chrono::milliseconds DelayedCallback::getWaitForExpirationDuration() const {
+    std::chrono::milliseconds delta = std::chrono::duration_cast<std::chrono::milliseconds>(
+            mExpiration - std::chrono::steady_clock::now());
+    // Return zero if this is already expired.
+    return delta > delta.zero() ? delta : delta.zero();
 }
 
 void DelayedCallback::run() const {
@@ -88,7 +91,9 @@
             mCondition.wait(mMutex);
         } else {
             // Wait until next callback expires, or a new one is scheduled.
-            mCondition.wait_until(mMutex, mQueue.top().getExpiration());
+            // Use the monotonic steady clock to wait for the measured delay interval via wait_for
+            // instead of using a wall clock via wait_until.
+            mCondition.wait_for(mMutex, mQueue.top().getWaitForExpirationDuration());
         }
     }
 }
diff --git a/services/vibratorservice/include/vibratorservice/VibratorCallbackScheduler.h b/services/vibratorservice/include/vibratorservice/VibratorCallbackScheduler.h
index 2c194b5..c8ec15d 100644
--- a/services/vibratorservice/include/vibratorservice/VibratorCallbackScheduler.h
+++ b/services/vibratorservice/include/vibratorservice/VibratorCallbackScheduler.h
@@ -30,15 +30,13 @@
 // Wrapper for a callback to be executed after a delay.
 class DelayedCallback {
 public:
-    using Timestamp = std::chrono::time_point<std::chrono::steady_clock>;
-
     DelayedCallback(std::function<void()> callback, std::chrono::milliseconds delay)
           : mCallback(callback), mExpiration(std::chrono::steady_clock::now() + delay) {}
     ~DelayedCallback() = default;
 
     void run() const;
     bool isExpired() const;
-    Timestamp getExpiration() const;
+    std::chrono::milliseconds getWaitForExpirationDuration() const;
 
     // Compare by expiration time, where A < B when A expires first.
     bool operator<(const DelayedCallback& other) const;
@@ -46,7 +44,9 @@
 
 private:
     std::function<void()> mCallback;
-    Timestamp mExpiration;
+    // Use a steady monotonic clock to calculate the duration until expiration.
+    // This clock is not related to wall clock time and is most suitable for measuring intervals.
+    std::chrono::time_point<std::chrono::steady_clock> mExpiration;
 };
 
 // Schedules callbacks to be executed after a delay.
diff --git a/services/vibratorservice/test/VibratorCallbackSchedulerTest.cpp b/services/vibratorservice/test/VibratorCallbackSchedulerTest.cpp
index 106ab9e..426cd42 100644
--- a/services/vibratorservice/test/VibratorCallbackSchedulerTest.cpp
+++ b/services/vibratorservice/test/VibratorCallbackSchedulerTest.cpp
@@ -71,15 +71,21 @@
     }
 
     int32_t waitForCallbacks(int32_t callbackCount, milliseconds timeout) {
-        time_point<steady_clock> expiration = steady_clock::now() + timeout + TEST_TIMEOUT;
+        time_point<steady_clock> expirationTime = steady_clock::now() + timeout + TEST_TIMEOUT;
         int32_t expiredCallbackCount = 0;
-        while (steady_clock::now() < expiration) {
+        while (steady_clock::now() < expirationTime) {
             std::lock_guard<std::mutex> lock(mMutex);
             expiredCallbackCount = mExpiredCallbacks.size();
             if (callbackCount <= expiredCallbackCount) {
                 return expiredCallbackCount;
             }
-            mCondition.wait_until(mMutex, expiration);
+            auto currentTimeout = std::chrono::duration_cast<std::chrono::milliseconds>(
+                    expirationTime - steady_clock::now());
+            if (currentTimeout > currentTimeout.zero()) {
+                // Use the monotonic steady clock to wait for the requested timeout via wait_for
+                // instead of using a wall clock via wait_until.
+                mCondition.wait_for(mMutex, currentTimeout);
+            }
         }
         return expiredCallbackCount;
     }
diff --git a/services/vr/virtual_touchpad/VirtualTouchpadService.cpp b/services/vr/virtual_touchpad/VirtualTouchpadService.cpp
index 523f890..d0a9da1 100644
--- a/services/vr/virtual_touchpad/VirtualTouchpadService.cpp
+++ b/services/vr/virtual_touchpad/VirtualTouchpadService.cpp
@@ -113,7 +113,7 @@
                         static_cast<long>(client_pid_));
     touchpad_->dumpInternal(result);
   }
-  write(fd, result.string(), result.size());
+  write(fd, result.c_str(), result.size());
   return OK;
 }