Merge "Define MotionEventFlag as an AIDL enum" into main
diff --git a/libs/adbd_auth/adbd_auth.cpp b/libs/adbd_auth/adbd_auth.cpp
index ebc74fb..78896ed 100644
--- a/libs/adbd_auth/adbd_auth.cpp
+++ b/libs/adbd_auth/adbd_auth.cpp
@@ -365,7 +365,7 @@
                         if (event.events & EPOLLIN) {
                             int rc = TEMP_FAILURE_RETRY(read(framework_fd_.get(), buf, sizeof(buf)));
                             if (rc == -1) {
-                                LOG(FATAL) << "adbd_auth: failed to read from framework fd";
+                                PLOG(FATAL) << "adbd_auth: failed to read from framework fd";
                             } else if (rc == 0) {
                                 LOG(INFO) << "adbd_auth: hit EOF on framework fd";
                                 std::lock_guard<std::mutex> lock(mutex_);
diff --git a/libs/binder/Android.bp b/libs/binder/Android.bp
index 7d15350..f31f8d3 100644
--- a/libs/binder/Android.bp
+++ b/libs/binder/Android.bp
@@ -19,6 +19,7 @@
     // to get the below license kinds:
     //   SPDX-license-identifier-Apache-2.0
     default_applicable_licenses: ["frameworks_native_license"],
+    default_team: "trendy_team_virtualization",
 }
 
 cc_library_headers {
@@ -90,7 +91,11 @@
         "libbinder_sdk",
         "libbinder_sdk_single_threaded",
         "libbinder_ndk_sdk",
+        "googletest_cmake",
+
         "binderRpcTestNoKernel",
+        "binderRpcTestSingleThreadedNoKernel",
+        "binderRpcWireProtocolTest",
     ],
     prebuilts: [
         // to enable arm64 host support, build with musl - e.g. on aosp_cf_arm64_phone
@@ -133,12 +138,16 @@
         {
             android_name: "libgtest",
             mapped_name: "GTest::gtest",
-            package_system: "GTest",
+            package_pregenerated: "external/googletest",
         },
         {
             android_name: "libgtest_main",
-            mapped_name: "GTest::gtest",
-            package_system: "GTest",
+            mapped_name: "GTest::gtest_main",
+            package_pregenerated: "external/googletest",
+        },
+        {
+            android_name: "googletest_cmake",
+            package_pregenerated: "external/googletest",
         },
 
         // use libbinder_sdk and friends instead of full Android's libbinder
diff --git a/libs/binder/IPCThreadState.cpp b/libs/binder/IPCThreadState.cpp
index fbc8125..c3bbdba 100644
--- a/libs/binder/IPCThreadState.cpp
+++ b/libs/binder/IPCThreadState.cpp
@@ -613,16 +613,19 @@
 
 void IPCThreadState::blockUntilThreadAvailable()
 {
-    pthread_mutex_lock(&mProcess->mThreadCountLock);
-    mProcess->mWaitingForThreads++;
-    while (mProcess->mExecutingThreadsCount >= mProcess->mMaxThreads) {
-        ALOGW("Waiting for thread to be free. mExecutingThreadsCount=%lu mMaxThreads=%lu\n",
-                static_cast<unsigned long>(mProcess->mExecutingThreadsCount),
-                static_cast<unsigned long>(mProcess->mMaxThreads));
-        pthread_cond_wait(&mProcess->mThreadCountDecrement, &mProcess->mThreadCountLock);
-    }
-    mProcess->mWaitingForThreads--;
-    pthread_mutex_unlock(&mProcess->mThreadCountLock);
+    std::unique_lock lock_guard_(mProcess->mOnThreadAvailableLock);
+    mProcess->mOnThreadAvailableWaiting++;
+    mProcess->mOnThreadAvailableCondVar.wait(lock_guard_, [&] {
+        size_t max = mProcess->mMaxThreads;
+        size_t cur = mProcess->mExecutingThreadsCount;
+        if (cur < max) {
+            return true;
+        }
+        ALOGW("Waiting for thread to be free. mExecutingThreadsCount=%zu mMaxThreads=%zu\n", cur,
+              max);
+        return false;
+    });
+    mProcess->mOnThreadAvailableWaiting--;
 }
 
 status_t IPCThreadState::getAndExecuteCommand()
@@ -642,34 +645,33 @@
             ALOGI("%s", message.c_str());
         }
 
-        pthread_mutex_lock(&mProcess->mThreadCountLock);
-        mProcess->mExecutingThreadsCount++;
-        if (mProcess->mExecutingThreadsCount >= mProcess->mMaxThreads &&
-                mProcess->mStarvationStartTimeMs == 0) {
-            mProcess->mStarvationStartTimeMs = uptimeMillis();
+        size_t newThreadsCount = mProcess->mExecutingThreadsCount.fetch_add(1) + 1;
+        if (newThreadsCount >= mProcess->mMaxThreads) {
+            int64_t expected = 0;
+            mProcess->mStarvationStartTimeMs.compare_exchange_strong(expected, uptimeMillis());
         }
-        pthread_mutex_unlock(&mProcess->mThreadCountLock);
 
         result = executeCommand(cmd);
 
-        pthread_mutex_lock(&mProcess->mThreadCountLock);
-        mProcess->mExecutingThreadsCount--;
-        if (mProcess->mExecutingThreadsCount < mProcess->mMaxThreads &&
-                mProcess->mStarvationStartTimeMs != 0) {
-            int64_t starvationTimeMs = uptimeMillis() - mProcess->mStarvationStartTimeMs;
-            if (starvationTimeMs > 100) {
-                ALOGE("binder thread pool (%zu threads) starved for %" PRId64 " ms",
-                      mProcess->mMaxThreads, starvationTimeMs);
+        size_t maxThreads = mProcess->mMaxThreads;
+        newThreadsCount = mProcess->mExecutingThreadsCount.fetch_sub(1) - 1;
+        if (newThreadsCount < maxThreads) {
+            size_t starvationStartTimeMs = mProcess->mStarvationStartTimeMs.exchange(0);
+            if (starvationStartTimeMs != 0) {
+                int64_t starvationTimeMs = uptimeMillis() - starvationStartTimeMs;
+                if (starvationTimeMs > 100) {
+                    ALOGE("binder thread pool (%zu threads) starved for %" PRId64 " ms", maxThreads,
+                          starvationTimeMs);
+                }
             }
-            mProcess->mStarvationStartTimeMs = 0;
         }
 
         // Cond broadcast can be expensive, so don't send it every time a binder
         // call is processed. b/168806193
-        if (mProcess->mWaitingForThreads > 0) {
-            pthread_cond_broadcast(&mProcess->mThreadCountDecrement);
+        if (mProcess->mOnThreadAvailableWaiting > 0) {
+            std::lock_guard lock_guard_(mProcess->mOnThreadAvailableLock);
+            mProcess->mOnThreadAvailableCondVar.notify_all();
         }
-        pthread_mutex_unlock(&mProcess->mThreadCountLock);
     }
 
     return result;
@@ -727,10 +729,9 @@
 
 void IPCThreadState::joinThreadPool(bool isMain)
 {
-    LOG_THREADPOOL("**** THREAD %p (PID %d) IS JOINING THE THREAD POOL\n", (void*)pthread_self(), getpid());
-    pthread_mutex_lock(&mProcess->mThreadCountLock);
+    LOG_THREADPOOL("**** THREAD %p (PID %d) IS JOINING THE THREAD POOL\n", (void*)pthread_self(),
+                   getpid());
     mProcess->mCurrentThreads++;
-    pthread_mutex_unlock(&mProcess->mThreadCountLock);
     mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER);
 
     mIsLooper = true;
@@ -758,13 +759,11 @@
     mOut.writeInt32(BC_EXIT_LOOPER);
     mIsLooper = false;
     talkWithDriver(false);
-    pthread_mutex_lock(&mProcess->mThreadCountLock);
-    LOG_ALWAYS_FATAL_IF(mProcess->mCurrentThreads == 0,
-                        "Threadpool thread count = 0. Thread cannot exist and exit in empty "
-                        "threadpool\n"
+    size_t oldCount = mProcess->mCurrentThreads.fetch_sub(1);
+    LOG_ALWAYS_FATAL_IF(oldCount == 0,
+                        "Threadpool thread count underflowed. Thread cannot exist and exit in "
+                        "empty threadpool\n"
                         "Misconfiguration. Increase threadpool max threads configuration\n");
-    mProcess->mCurrentThreads--;
-    pthread_mutex_unlock(&mProcess->mThreadCountLock);
 }
 
 status_t IPCThreadState::setupPolling(int* fd)
@@ -776,9 +775,7 @@
     mOut.writeInt32(BC_ENTER_LOOPER);
     flushCommands();
     *fd = mProcess->mDriverFD;
-    pthread_mutex_lock(&mProcess->mThreadCountLock);
     mProcess->mCurrentThreads++;
-    pthread_mutex_unlock(&mProcess->mThreadCountLock);
     return 0;
 }
 
diff --git a/libs/binder/ProcessState.cpp b/libs/binder/ProcessState.cpp
index 8485ecd..ad5a6b3 100644
--- a/libs/binder/ProcessState.cpp
+++ b/libs/binder/ProcessState.cpp
@@ -407,9 +407,7 @@
         ALOGV("Spawning new pooled thread, name=%s\n", name.c_str());
         sp<Thread> t = sp<PoolThread>::make(isMain);
         t->run(name.c_str());
-        pthread_mutex_lock(&mThreadCountLock);
         mKernelStartedThreads++;
-        pthread_mutex_unlock(&mThreadCountLock);
     }
     // TODO: if startThreadPool is called on another thread after the process
     // starts up, the kernel might think that it already requested those
@@ -432,19 +430,19 @@
 }
 
 size_t ProcessState::getThreadPoolMaxTotalThreadCount() const {
-    pthread_mutex_lock(&mThreadCountLock);
-    auto detachGuard = make_scope_guard([&]() { pthread_mutex_unlock(&mThreadCountLock); });
-
     if (mThreadPoolStarted) {
-        LOG_ALWAYS_FATAL_IF(mKernelStartedThreads > mMaxThreads + 1,
-                            "too many kernel-started threads: %zu > %zu + 1", mKernelStartedThreads,
-                            mMaxThreads);
+        size_t kernelStarted = mKernelStartedThreads;
+        size_t max = mMaxThreads;
+        size_t current = mCurrentThreads;
+
+        LOG_ALWAYS_FATAL_IF(kernelStarted > max + 1,
+                            "too many kernel-started threads: %zu > %zu + 1", kernelStarted, max);
 
         // calling startThreadPool starts a thread
         size_t threads = 1;
 
         // the kernel is configured to start up to mMaxThreads more threads
-        threads += mMaxThreads;
+        threads += max;
 
         // Users may call IPCThreadState::joinThreadPool directly. We don't
         // currently have a way to count this directly (it could be added by
@@ -454,8 +452,8 @@
         // in IPCThreadState, temporarily forget about the extra join threads.
         // This is okay, because most callers of this method only care about
         // having 0, 1, or more threads.
-        if (mCurrentThreads > mKernelStartedThreads) {
-            threads += mCurrentThreads - mKernelStartedThreads;
+        if (current > kernelStarted) {
+            threads += current - kernelStarted;
         }
 
         return threads;
@@ -463,10 +461,9 @@
 
     // must not be initialized or maybe has poll thread setup, we
     // currently don't track this in libbinder
-    LOG_ALWAYS_FATAL_IF(mKernelStartedThreads != 0,
-                        "Expecting 0 kernel started threads but have"
-                        " %zu",
-                        mKernelStartedThreads);
+    size_t kernelStarted = mKernelStartedThreads;
+    LOG_ALWAYS_FATAL_IF(kernelStarted != 0, "Expecting 0 kernel started threads but have %zu",
+                        kernelStarted);
     return mCurrentThreads;
 }
 
@@ -554,10 +551,7 @@
       : mDriverName(String8(driver)),
         mDriverFD(-1),
         mVMStart(MAP_FAILED),
-        mThreadCountLock(PTHREAD_MUTEX_INITIALIZER),
-        mThreadCountDecrement(PTHREAD_COND_INITIALIZER),
         mExecutingThreadsCount(0),
-        mWaitingForThreads(0),
         mMaxThreads(DEFAULT_MAX_BINDER_THREADS),
         mCurrentThreads(0),
         mKernelStartedThreads(0),
diff --git a/libs/binder/include/binder/ProcessState.h b/libs/binder/include/binder/ProcessState.h
index a466638..11898a0 100644
--- a/libs/binder/include/binder/ProcessState.h
+++ b/libs/binder/include/binder/ProcessState.h
@@ -23,6 +23,7 @@
 
 #include <pthread.h>
 
+#include <atomic>
 #include <mutex>
 
 // ---------------------------------------------------------------------------
@@ -162,22 +163,21 @@
     int mDriverFD;
     void* mVMStart;
 
-    // Protects thread count and wait variables below.
-    mutable pthread_mutex_t mThreadCountLock;
-    // Broadcast whenever mWaitingForThreads > 0
-    pthread_cond_t mThreadCountDecrement;
+    mutable std::mutex mOnThreadAvailableLock;
+    std::condition_variable mOnThreadAvailableCondVar;
+    // Number of threads waiting on `mOnThreadAvailableCondVar`.
+    std::atomic_int64_t mOnThreadAvailableWaiting = 0;
+
     // Number of binder threads current executing a command.
-    size_t mExecutingThreadsCount;
-    // Number of threads calling IPCThreadState::blockUntilThreadAvailable()
-    size_t mWaitingForThreads;
+    std::atomic_size_t mExecutingThreadsCount;
     // Maximum number of lazy threads to be started in the threadpool by the kernel.
-    size_t mMaxThreads;
+    std::atomic_size_t mMaxThreads;
     // Current number of threads inside the thread pool.
-    size_t mCurrentThreads;
+    std::atomic_size_t mCurrentThreads;
     // Current number of pooled threads inside the thread pool.
-    size_t mKernelStartedThreads;
+    std::atomic_size_t mKernelStartedThreads;
     // Time when thread pool was emptied
-    int64_t mStarvationStartTimeMs;
+    std::atomic_int64_t mStarvationStartTimeMs;
 
     mutable std::mutex mLock; // protects everything below.
 
diff --git a/libs/binder/ndk/include_cpp/android/persistable_bundle_aidl.h b/libs/binder/ndk/include_cpp/android/persistable_bundle_aidl.h
index d570eab..cf7dc1a 100644
--- a/libs/binder/ndk/include_cpp/android/persistable_bundle_aidl.h
+++ b/libs/binder/ndk/include_cpp/android/persistable_bundle_aidl.h
@@ -26,13 +26,10 @@
 #if defined(__ANDROID_VENDOR__)
 #include <android/llndk-versioning.h>
 #else  // __ANDROID_VENDOR__
-#if defined(API_LEVEL_AT_LEAST)
-// Redefine API_LEVEL_AT_LEAST here to replace the version to __ANDROID_API_FUTURE__ as a workaround
-#undef API_LEVEL_AT_LEAST
-#endif
-// TODO(b/322384429) switch this __ANDROID_API_FUTURE__ to sdk_api_level when V is finalized
+#if !defined(API_LEVEL_AT_LEAST)
 #define API_LEVEL_AT_LEAST(sdk_api_level, vendor_api_level) \
-    (__builtin_available(android __ANDROID_API_FUTURE__, *))
+    (__builtin_available(android sdk_api_level, *))
+#endif
 #endif  // __ANDROID_VENDOR__
 
 namespace aidl::android::os {
diff --git a/libs/binder/tests/binder_sdk/Android.bp b/libs/binder/tests/binder_sdk/Android.bp
new file mode 100644
index 0000000..4e884ad
--- /dev/null
+++ b/libs/binder/tests/binder_sdk/Android.bp
@@ -0,0 +1,84 @@
+//
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_native_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_native_license"],
+}
+
+sh_test_host {
+    name: "binder_sdk_test",
+    src: "binder_sdk_test.sh",
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: false,
+    },
+
+    data: [
+        ":binder_sdk",
+        ":cmake_root",
+    ],
+    data_bins: [
+        "cmake",
+        "ctest",
+    ],
+}
+
+sh_test_host {
+    name: "binder_sdk_docker_test_gcc",
+    src: "binder_sdk_docker_test.sh",
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: false,
+    },
+
+    data: [
+        ":binder_sdk",
+        "gcc.Dockerfile",
+    ],
+}
+
+sh_test_host {
+    name: "binder_sdk_docker_test_clang",
+    src: "binder_sdk_docker_test.sh",
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: false,
+    },
+
+    data: [
+        ":binder_sdk",
+        "clang.Dockerfile",
+    ],
+}
+
+sh_test_host {
+    name: "binder_sdk_docker_test_gnumake",
+    src: "binder_sdk_docker_test.sh",
+    test_suites: ["general-tests"],
+    test_options: {
+        unit_test: false,
+    },
+
+    data: [
+        ":binder_sdk",
+        "gnumake.Dockerfile",
+    ],
+}
diff --git a/libs/binder/tests/binder_sdk/binder_sdk_docker_test.sh b/libs/binder/tests/binder_sdk/binder_sdk_docker_test.sh
new file mode 100755
index 0000000..0eca846
--- /dev/null
+++ b/libs/binder/tests/binder_sdk/binder_sdk_docker_test.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+#
+# Copyright (C) 2024 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.
+#
+
+set -ex
+
+TEST_NAME="$(basename "$0")"
+DOCKER_TAG="${TEST_NAME}-${RANDOM}${RANDOM}"
+DOCKER_FILE=*.Dockerfile
+DOCKER_RUN_FLAGS=
+
+# Guess if we're running as an Android test or directly
+if [ "$(ls -1 ${DOCKER_FILE} | wc -l)" == "1" ]; then
+    # likely running as `atest binder_sdk_docker_test_XYZ`
+    DOCKER_PATH="$(dirname $(readlink --canonicalize --no-newline binder_sdk.zip))"
+else
+    # likely running directly as `./binder_sdk_docker_test.sh` - provide mode for easy testing
+    RED='\033[1;31m'
+    NO_COLOR='\033[0m'
+
+    if ! modinfo vsock_loopback &>/dev/null ; then
+        echo -e "${RED}Module vsock_loopback is not installed.${NO_COLOR}"
+        exit 1
+    fi
+    if modprobe --dry-run --first-time vsock_loopback &>/dev/null ; then
+        echo "Module vsock_loopback is not loaded. Attempting to load..."
+        if ! sudo modprobe vsock_loopback ; then
+            echo -e "${RED}Module vsock_loopback is not loaded and attempt to load failed.${NO_COLOR}"
+            exit 1
+        fi
+    fi
+
+    DOCKER_RUN_FLAGS="--interactive --tty"
+
+    DOCKER_FILE="$1"
+    if [ ! -f "${DOCKER_FILE}" ]; then
+        echo -e "${RED}Docker file '${DOCKER_FILE}' doesn't exist. Please provide one as an argument.${NO_COLOR}"
+        exit 1
+    fi
+
+    if [ ! -d "${ANDROID_BUILD_TOP}" ]; then
+        echo -e "${RED}ANDROID_BUILD_TOP doesn't exist. Please lunch some target.${NO_COLOR}"
+        exit 1
+    fi
+    ${ANDROID_BUILD_TOP}/build/soong/soong_ui.bash --make-mode binder_sdk
+    BINDER_SDK_ZIP="${ANDROID_BUILD_TOP}/out/soong/.intermediates/frameworks/native/libs/binder/binder_sdk/linux_glibc_x86_64/*/binder_sdk.zip"
+    DOCKER_PATH="$(dirname $(ls -1 ${BINDER_SDK_ZIP} | head --lines=1))"
+fi
+
+function cleanup {
+    docker rmi --force "${DOCKER_TAG}" 2>/dev/null || true
+}
+trap cleanup EXIT
+
+docker build --force-rm --tag "${DOCKER_TAG}" --file ${DOCKER_FILE} ${DOCKER_PATH}
+docker run ${DOCKER_RUN_FLAGS} --rm "${DOCKER_TAG}"
diff --git a/libs/binder/tests/binder_sdk/binder_sdk_test.sh b/libs/binder/tests/binder_sdk/binder_sdk_test.sh
new file mode 100644
index 0000000..1881ace
--- /dev/null
+++ b/libs/binder/tests/binder_sdk/binder_sdk_test.sh
@@ -0,0 +1,40 @@
+#!/bin/bash
+
+#
+# Copyright (C) 2024 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.
+#
+
+set -ex
+
+RED='\033[1;31m'
+NO_COLOR='\033[0m'
+
+if [ ! -f "binder_sdk.zip" ]; then
+    echo -e "${RED}binder_sdk.zip doesn't exist. Are you running this test through 'atest binder_sdk_test'?${NO_COLOR}"
+    exit 1
+fi
+
+mkdir -p bin
+cp `pwd`/cmake bin/cmake
+cp `pwd`/ctest bin/ctest
+export PATH="`pwd`/bin:$PATH"
+
+WORKDIR=workdir_$RANDOM$RANDOM$RANDOM
+unzip -q -d $WORKDIR binder_sdk.zip
+cd $WORKDIR
+
+cmake .
+make -j
+make test ARGS="--parallel 32 --output-on-failure"
diff --git a/libs/binder/tests/binder_sdk/clang.Dockerfile b/libs/binder/tests/binder_sdk/clang.Dockerfile
new file mode 100644
index 0000000..aa1fec2
--- /dev/null
+++ b/libs/binder/tests/binder_sdk/clang.Dockerfile
@@ -0,0 +1,32 @@
+#
+# Copyright (C) 2024 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.
+#
+
+FROM debian:bookworm
+
+RUN echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list && \
+    apt-get update -y && \
+    apt-get install -y clang cmake ninja-build unzip
+
+ADD binder_sdk.zip /
+RUN unzip -q -d binder_sdk binder_sdk.zip
+
+WORKDIR /binder_sdk
+RUN CC=clang CXX=clang++ cmake -G Ninja -B build .
+RUN cmake --build build
+
+WORKDIR /binder_sdk/build
+# Alternatively: `ninja test`, but it won't pass parallel argument
+ENTRYPOINT [ "ctest", "--parallel", "32", "--output-on-failure" ]
diff --git a/libs/binder/tests/binder_sdk/gcc.Dockerfile b/libs/binder/tests/binder_sdk/gcc.Dockerfile
new file mode 100644
index 0000000..fb2ee2c
--- /dev/null
+++ b/libs/binder/tests/binder_sdk/gcc.Dockerfile
@@ -0,0 +1,32 @@
+#
+# Copyright (C) 2024 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.
+#
+
+FROM gcc:9
+
+RUN echo 'deb http://deb.debian.org/debian bullseye-backports main' >> /etc/apt/sources.list && \
+    apt-get update -y && \
+    apt-get install -y cmake ninja-build
+
+ADD binder_sdk.zip /
+RUN unzip -q -d binder_sdk binder_sdk.zip
+
+WORKDIR /binder_sdk
+RUN CC=gcc CXX=g++ cmake -G Ninja -B build .
+RUN cmake --build build
+
+WORKDIR /binder_sdk/build
+# Alternatively: `ninja test`, but it won't pass parallel argument
+ENTRYPOINT [ "ctest", "--parallel", "32", "--output-on-failure" ]
diff --git a/libs/binder/tests/binder_sdk/gnumake.Dockerfile b/libs/binder/tests/binder_sdk/gnumake.Dockerfile
new file mode 100644
index 0000000..abe12fb
--- /dev/null
+++ b/libs/binder/tests/binder_sdk/gnumake.Dockerfile
@@ -0,0 +1,30 @@
+#
+# Copyright (C) 2024 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.
+#
+
+FROM gcc:9
+
+RUN echo 'deb http://deb.debian.org/debian bullseye-backports main' >> /etc/apt/sources.list && \
+    apt-get update -y && \
+    apt-get install -y cmake
+
+ADD binder_sdk.zip /
+RUN unzip -q -d binder_sdk binder_sdk.zip
+
+WORKDIR /binder_sdk
+RUN cmake .
+RUN make -j
+
+ENTRYPOINT make test ARGS="--parallel 32 --output-on-failure"
diff --git a/libs/input/rust/keyboard_classification_config.rs b/libs/input/rust/keyboard_classification_config.rs
new file mode 100644
index 0000000..ab74efb
--- /dev/null
+++ b/libs/input/rust/keyboard_classification_config.rs
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2024 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 crate::input::KeyboardType;
+
+// TODO(b/263559234): Categorize some of these to KeyboardType::None based on ability to produce
+//  key events at all. (Requires setup allowing InputDevice to dynamically add/remove
+//  KeyboardInputMapper based on blocklist and KeyEvents in case a KeyboardType::None device ends
+//  up producing a key event)
+pub static CLASSIFIED_DEVICES: &[(
+    /* vendorId */ u16,
+    /* productId */ u16,
+    KeyboardType,
+    /* is_finalized */ bool,
+)] = &[
+    // HP X4000 Wireless Mouse
+    (0x03f0, 0xa407, KeyboardType::NonAlphabetic, true),
+    // Microsoft Wireless Mobile Mouse 6000
+    (0x045e, 0x0745, KeyboardType::NonAlphabetic, true),
+    // Microsoft Surface Precision Mouse
+    (0x045e, 0x0821, KeyboardType::NonAlphabetic, true),
+    // Microsoft Pro IntelliMouse
+    (0x045e, 0x082a, KeyboardType::NonAlphabetic, true),
+    // Microsoft Bluetooth Mouse
+    (0x045e, 0x082f, KeyboardType::NonAlphabetic, true),
+    // Xbox One Elite Series 2 gamepad
+    (0x045e, 0x0b05, KeyboardType::NonAlphabetic, true),
+    // Logitech T400
+    (0x046d, 0x4026, KeyboardType::NonAlphabetic, true),
+    // Logitech M720 Triathlon (Unifying)
+    (0x046d, 0x405e, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Master 2S (Unifying)
+    (0x046d, 0x4069, KeyboardType::NonAlphabetic, true),
+    // Logitech M585 (Unifying)
+    (0x046d, 0x406b, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Anywhere 2 (Unifying)
+    (0x046d, 0x4072, KeyboardType::NonAlphabetic, true),
+    // Logitech Pebble M350
+    (0x046d, 0x4080, KeyboardType::NonAlphabetic, true),
+    // Logitech T630 Ultrathin
+    (0x046d, 0xb00d, KeyboardType::NonAlphabetic, true),
+    // Logitech M558
+    (0x046d, 0xb011, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Master (Bluetooth)
+    (0x046d, 0xb012, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Anywhere 2 (Bluetooth)
+    (0x046d, 0xb013, KeyboardType::NonAlphabetic, true),
+    // Logitech M720 Triathlon (Bluetooth)
+    (0x046d, 0xb015, KeyboardType::NonAlphabetic, true),
+    // Logitech M535
+    (0x046d, 0xb016, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Master / Anywhere 2 (Bluetooth)
+    (0x046d, 0xb017, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Master 2S (Bluetooth)
+    (0x046d, 0xb019, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Anywhere 2S (Bluetooth)
+    (0x046d, 0xb01a, KeyboardType::NonAlphabetic, true),
+    // Logitech M585/M590 (Bluetooth)
+    (0x046d, 0xb01b, KeyboardType::NonAlphabetic, true),
+    // Logitech G603 Lightspeed Gaming Mouse (Bluetooth)
+    (0x046d, 0xb01c, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Master (Bluetooth)
+    (0x046d, 0xb01e, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Anywhere 2 (Bluetooth)
+    (0x046d, 0xb01f, KeyboardType::NonAlphabetic, true),
+    // Logitech MX Master 3 (Bluetooth)
+    (0x046d, 0xb023, KeyboardType::NonAlphabetic, true),
+    // Logitech G604 Lightspeed Gaming Mouse (Bluetooth)
+    (0x046d, 0xb024, KeyboardType::NonAlphabetic, true),
+    // Logitech Spotlight Presentation Remote (Bluetooth)
+    (0x046d, 0xb503, KeyboardType::NonAlphabetic, true),
+    // Logitech R500 (Bluetooth)
+    (0x046d, 0xb505, KeyboardType::NonAlphabetic, true),
+    // Logitech M500s
+    (0x046d, 0xc093, KeyboardType::NonAlphabetic, true),
+    // Logitech Spotlight Presentation Remote (USB dongle)
+    (0x046d, 0xc53e, KeyboardType::NonAlphabetic, true),
+    // Elecom Enelo IR LED Mouse 350
+    (0x056e, 0x0134, KeyboardType::NonAlphabetic, true),
+    // Elecom EPRIM Blue LED 5 button mouse 228
+    (0x056e, 0x0141, KeyboardType::NonAlphabetic, true),
+    // Elecom Blue LED Mouse 203
+    (0x056e, 0x0159, KeyboardType::NonAlphabetic, true),
+    // Zebra LS2208 barcode scanner
+    (0x05e0, 0x1200, KeyboardType::NonAlphabetic, true),
+    // RDing FootSwitch1F1
+    (0x0c45, 0x7403, KeyboardType::NonAlphabetic, true),
+    // SteelSeries Sensei RAW Frost Blue
+    (0x1038, 0x1369, KeyboardType::NonAlphabetic, true),
+    // SteelSeries Rival 3 Wired
+    (0x1038, 0x1824, KeyboardType::NonAlphabetic, true),
+    // SteelSeries Rival 3 Wireless (USB dongle)
+    (0x1038, 0x1830, KeyboardType::NonAlphabetic, true),
+    // Yubico.com Yubikey
+    (0x1050, 0x0010, KeyboardType::NonAlphabetic, true),
+    // Yubico.com Yubikey 4 OTP+U2F+CCID
+    (0x1050, 0x0407, KeyboardType::NonAlphabetic, true),
+    // Lenovo USB-C Wired Compact Mouse
+    (0x17ef, 0x6123, KeyboardType::NonAlphabetic, true),
+    // Corsair Katar Pro Wireless (USB dongle)
+    (0x1b1c, 0x1b94, KeyboardType::NonAlphabetic, true),
+    // Corsair Katar Pro Wireless (Bluetooth)
+    (0x1bae, 0x1b1c, KeyboardType::NonAlphabetic, true),
+    // Kensington Pro Fit Full-size
+    (0x1bcf, 0x08a0, KeyboardType::NonAlphabetic, true),
+    // Huion HS64
+    (0x256c, 0x006d, KeyboardType::NonAlphabetic, true),
+    // XP-Pen Star G640
+    (0x28bd, 0x0914, KeyboardType::NonAlphabetic, true),
+    // XP-Pen Artist 12 Pro
+    (0x28bd, 0x091f, KeyboardType::NonAlphabetic, true),
+    // XP-Pen Deco mini7W
+    (0x28bd, 0x0928, KeyboardType::NonAlphabetic, true),
+];
diff --git a/libs/input/rust/keyboard_classifier.rs b/libs/input/rust/keyboard_classifier.rs
index 1063fac..8721ef7 100644
--- a/libs/input/rust/keyboard_classifier.rs
+++ b/libs/input/rust/keyboard_classifier.rs
@@ -35,6 +35,7 @@
 //! TODO(b/263559234): Data store implementation to store information about past classification
 
 use crate::input::{DeviceId, InputDevice, KeyboardType};
+use crate::keyboard_classification_config::CLASSIFIED_DEVICES;
 use crate::{DeviceClass, ModifierState};
 use std::collections::HashMap;
 
@@ -126,6 +127,14 @@
                 (KeyboardType::NonAlphabetic, true)
             };
         }
+
+        // Check in known device list for classification
+        for data in CLASSIFIED_DEVICES.iter() {
+            if device.identifier.vendor == data.0 && device.identifier.product == data.1 {
+                return (data.2, data.3);
+            }
+        }
+
         // Any composite device with multiple device classes should be categorized as non-alphabetic
         // keyboard initially
         if device.classes.contains(DeviceClass::Touch)
@@ -169,6 +178,7 @@
 #[cfg(test)]
 mod tests {
     use crate::input::{DeviceId, InputDevice, KeyboardType};
+    use crate::keyboard_classification_config::CLASSIFIED_DEVICES;
     use crate::keyboard_classifier::KeyboardClassifier;
     use crate::{DeviceClass, ModifierState, RustInputDeviceIdentifier};
 
@@ -326,6 +336,17 @@
         assert!(!classifier.is_finalized(DEVICE_ID));
     }
 
+    #[test]
+    fn classify_known_devices() {
+        let mut classifier = KeyboardClassifier::new();
+        for device in CLASSIFIED_DEVICES.iter() {
+            classifier
+                .notify_keyboard_changed(create_device_with_vendor_product_ids(device.0, device.1));
+            assert_eq!(classifier.get_keyboard_type(DEVICE_ID), device.2);
+            assert_eq!(classifier.is_finalized(DEVICE_ID), device.3);
+        }
+    }
+
     fn create_device(classes: DeviceClass) -> InputDevice {
         InputDevice {
             device_id: DEVICE_ID,
@@ -342,4 +363,21 @@
             classes,
         }
     }
+
+    fn create_device_with_vendor_product_ids(vendor: u16, product: u16) -> InputDevice {
+        InputDevice {
+            device_id: DEVICE_ID,
+            identifier: RustInputDeviceIdentifier {
+                name: "test_device".to_string(),
+                location: "location".to_string(),
+                unique_id: "unique_id".to_string(),
+                bus: 123,
+                vendor,
+                product,
+                version: 567,
+                descriptor: "descriptor".to_string(),
+            },
+            classes: DeviceClass::Keyboard | DeviceClass::AlphabeticKey | DeviceClass::External,
+        }
+    }
 }
diff --git a/libs/input/rust/lib.rs b/libs/input/rust/lib.rs
index 5010475..af8f889 100644
--- a/libs/input/rust/lib.rs
+++ b/libs/input/rust/lib.rs
@@ -18,6 +18,7 @@
 
 mod input;
 mod input_verifier;
+mod keyboard_classification_config;
 mod keyboard_classifier;
 
 pub use input::{
diff --git a/libs/ui/Gralloc5.cpp b/libs/ui/Gralloc5.cpp
index f14a5cf..c9ec036 100644
--- a/libs/ui/Gralloc5.cpp
+++ b/libs/ui/Gralloc5.cpp
@@ -91,8 +91,7 @@
         }
 
         void* so = nullptr;
-        // TODO(b/322384429) switch this to __ANDROID_API_V__ when V is finalized
-        if API_LEVEL_AT_LEAST(__ANDROID_API_FUTURE__, 202404) {
+        if API_LEVEL_AT_LEAST (__ANDROID_API_V__, 202404) {
             so = AServiceManager_openDeclaredPassthroughHal("mapper", mapperSuffix.c_str(),
                                                             RTLD_LOCAL | RTLD_NOW);
         } else {
diff --git a/services/surfaceflinger/Scheduler/Scheduler.cpp b/services/surfaceflinger/Scheduler/Scheduler.cpp
index 60681a2..26e11e5 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.cpp
+++ b/services/surfaceflinger/Scheduler/Scheduler.cpp
@@ -123,19 +123,22 @@
     promotePacesetterDisplay(pacesetterIdOpt);
 }
 
-void Scheduler::registerDisplay(PhysicalDisplayId displayId, RefreshRateSelectorPtr selectorPtr) {
+void Scheduler::registerDisplay(PhysicalDisplayId displayId, RefreshRateSelectorPtr selectorPtr,
+                                PhysicalDisplayId activeDisplayId) {
     auto schedulePtr =
             std::make_shared<VsyncSchedule>(selectorPtr->getActiveMode().modePtr, mFeatures,
                                             [this](PhysicalDisplayId id, bool enable) {
                                                 onHardwareVsyncRequest(id, enable);
                                             });
 
-    registerDisplayInternal(displayId, std::move(selectorPtr), std::move(schedulePtr));
+    registerDisplayInternal(displayId, std::move(selectorPtr), std::move(schedulePtr),
+                            activeDisplayId);
 }
 
 void Scheduler::registerDisplayInternal(PhysicalDisplayId displayId,
                                         RefreshRateSelectorPtr selectorPtr,
-                                        VsyncSchedulePtr schedulePtr) {
+                                        VsyncSchedulePtr schedulePtr,
+                                        PhysicalDisplayId activeDisplayId) {
     demotePacesetterDisplay();
 
     auto [pacesetterVsyncSchedule, isNew] = [&]() FTL_FAKE_GUARD(kMainThreadContext) {
@@ -145,7 +148,7 @@
                                                        std::move(schedulePtr), mFeatures)
                                    .second;
 
-        return std::make_pair(promotePacesetterDisplayLocked(), isNew);
+        return std::make_pair(promotePacesetterDisplayLocked(activeDisplayId), isNew);
     }();
 
     applyNewVsyncSchedule(std::move(pacesetterVsyncSchedule));
@@ -158,7 +161,9 @@
     dispatchHotplug(displayId, Hotplug::Connected);
 }
 
-void Scheduler::unregisterDisplay(PhysicalDisplayId displayId) {
+void Scheduler::unregisterDisplay(PhysicalDisplayId displayId, PhysicalDisplayId activeDisplayId) {
+    LOG_ALWAYS_FATAL_IF(displayId == activeDisplayId, "Cannot unregister the active display!");
+
     dispatchHotplug(displayId, Hotplug::Disconnected);
 
     demotePacesetterDisplay();
@@ -173,7 +178,7 @@
         // headless virtual display.)
         LOG_ALWAYS_FATAL_IF(mDisplays.empty(), "Cannot unregister all displays!");
 
-        pacesetterVsyncSchedule = promotePacesetterDisplayLocked();
+        pacesetterVsyncSchedule = promotePacesetterDisplayLocked(activeDisplayId);
     }
     applyNewVsyncSchedule(std::move(pacesetterVsyncSchedule));
 }
diff --git a/services/surfaceflinger/Scheduler/Scheduler.h b/services/surfaceflinger/Scheduler/Scheduler.h
index ccaa05f..1a4aa79 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.h
+++ b/services/surfaceflinger/Scheduler/Scheduler.h
@@ -101,9 +101,16 @@
     using ConstVsyncSchedulePtr = std::shared_ptr<const VsyncSchedule>;
     using VsyncSchedulePtr = std::shared_ptr<VsyncSchedule>;
 
-    void registerDisplay(PhysicalDisplayId, RefreshRateSelectorPtr) REQUIRES(kMainThreadContext)
+    // After registration/unregistration, `activeDisplayId` is promoted to pacesetter. Note that the
+    // active display is never unregistered, since hotplug disconnect never happens for activatable
+    // displays, i.e. a foldable's internal displays or otherwise the (internal or external) primary
+    // display.
+    // TODO: b/255635821 - Remove active display parameters.
+    void registerDisplay(PhysicalDisplayId, RefreshRateSelectorPtr,
+                         PhysicalDisplayId activeDisplayId) REQUIRES(kMainThreadContext)
             EXCLUDES(mDisplayLock);
-    void unregisterDisplay(PhysicalDisplayId) REQUIRES(kMainThreadContext) EXCLUDES(mDisplayLock);
+    void unregisterDisplay(PhysicalDisplayId, PhysicalDisplayId activeDisplayId)
+            REQUIRES(kMainThreadContext) EXCLUDES(mDisplayLock);
 
     void run();
 
@@ -390,8 +397,9 @@
     // the caller on the main thread to avoid deadlock, since the timer thread locks it before exit.
     void demotePacesetterDisplay() REQUIRES(kMainThreadContext) EXCLUDES(mDisplayLock, mPolicyLock);
 
-    void registerDisplayInternal(PhysicalDisplayId, RefreshRateSelectorPtr, VsyncSchedulePtr)
-            REQUIRES(kMainThreadContext) EXCLUDES(mDisplayLock);
+    void registerDisplayInternal(PhysicalDisplayId, RefreshRateSelectorPtr, VsyncSchedulePtr,
+                                 PhysicalDisplayId activeDisplayId) REQUIRES(kMainThreadContext)
+            EXCLUDES(mDisplayLock);
 
     struct Policy;
 
diff --git a/services/surfaceflinger/Scheduler/VSyncPredictor.cpp b/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
index dd3c4b0..0644aca 100644
--- a/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
+++ b/services/surfaceflinger/Scheduler/VSyncPredictor.cpp
@@ -360,7 +360,11 @@
     purgeTimelines(now);
 
     for (auto& timeline : mTimelines) {
-        if (timeline.validUntil() && timeline.validUntil()->ns() > vsync) {
+        const bool isVsyncValid = FlagManager::getInstance().vrr_bugfix_24q4()
+                ? timeline.isWithin(TimePoint::fromNs(vsync)) ==
+                        VsyncTimeline::VsyncOnTimeline::Unique
+                : timeline.validUntil() && timeline.validUntil()->ns() > vsync;
+        if (isVsyncValid) {
             return timeline.isVSyncInPhase(model, vsync, frameRate);
         }
     }
@@ -395,8 +399,14 @@
         mLastCommittedVsync = TimePoint::fromNs(0);
 
     } else {
-        mTimelines.back().freeze(
-                TimePoint::fromNs(mLastCommittedVsync.ns() + mIdealPeriod.ns() / 2));
+        if (FlagManager::getInstance().vrr_bugfix_24q4()) {
+            // We need to freeze the timeline at the committed vsync so that we don't
+            // overshoot the deadline.
+            mTimelines.back().freeze(mLastCommittedVsync);
+        } else {
+            mTimelines.back().freeze(
+                    TimePoint::fromNs(mLastCommittedVsync.ns() + mIdealPeriod.ns() / 2));
+        }
     }
     mTimelines.emplace_back(mLastCommittedVsync, mIdealPeriod, renderRate);
     purgeTimelines(TimePoint::fromNs(mClock->now()));
@@ -611,7 +621,10 @@
 
     while (mTimelines.size() > 1) {
         const auto validUntilOpt = mTimelines.front().validUntil();
-        if (validUntilOpt && *validUntilOpt < now) {
+        const bool isTimelineOutDated = FlagManager::getInstance().vrr_bugfix_24q4()
+                ? mTimelines.front().isWithin(now) == VsyncTimeline::VsyncOnTimeline::Outside
+                : validUntilOpt && *validUntilOpt < now;
+        if (isTimelineOutDated) {
             mTimelines.pop_front();
         } else {
             break;
@@ -660,9 +673,12 @@
             vsyncTime += missedVsync.fixup.ns();
             ATRACE_FORMAT_INSTANT("lastFrameMissed");
         } else if (mightBackpressure && lastVsyncOpt) {
-            // lastVsyncOpt is based on the old timeline before we shifted it. we should correct it
-            // first before trying to use it.
-            lastVsyncOpt = snapToVsyncAlignedWithRenderRate(model, *lastVsyncOpt);
+            if (!FlagManager::getInstance().vrr_bugfix_24q4()) {
+                // lastVsyncOpt does not need to be corrected with the new rate, and
+                // it should be used as is to avoid skipping a frame when changing rates are
+                // aligned at vsync time.
+                lastVsyncOpt = snapToVsyncAlignedWithRenderRate(model, *lastVsyncOpt);
+            }
             const auto vsyncDiff = vsyncTime - *lastVsyncOpt;
             if (vsyncDiff <= minFramePeriodOpt->ns() - threshold) {
                 // avoid a duplicate vsync
@@ -681,7 +697,10 @@
     }
 
     ATRACE_FORMAT_INSTANT("vsync in %.2fms", float(vsyncTime - TimePoint::now().ns()) / 1e6f);
-    if (mValidUntil && vsyncTime > mValidUntil->ns()) {
+    const bool isVsyncInvalid = FlagManager::getInstance().vrr_bugfix_24q4()
+            ? isWithin(TimePoint::fromNs(vsyncTime)) == VsyncOnTimeline::Outside
+            : mValidUntil && vsyncTime > mValidUntil->ns();
+    if (isVsyncInvalid) {
         ATRACE_FORMAT_INSTANT("no longer valid for vsync in %.2f",
                               static_cast<float>(vsyncTime - TimePoint::now().ns()) / 1e6f);
         return std::nullopt;
diff --git a/services/surfaceflinger/Scheduler/VSyncPredictor.h b/services/surfaceflinger/Scheduler/VSyncPredictor.h
index 8ce61d8..66a7d71 100644
--- a/services/surfaceflinger/Scheduler/VSyncPredictor.h
+++ b/services/surfaceflinger/Scheduler/VSyncPredictor.h
@@ -106,6 +106,24 @@
         void shiftVsyncSequence(Duration phase);
         void setRenderRate(std::optional<Fps> renderRateOpt) { mRenderRateOpt = renderRateOpt; }
 
+        enum class VsyncOnTimeline {
+            Unique,  // Within timeline, not shared with next timeline.
+            Shared,  // Within timeline, shared with next timeline.
+            Outside, // Outside of the timeline.
+        };
+        VsyncOnTimeline isWithin(TimePoint vsync) {
+            const auto threshold = mIdealPeriod.ns() / 2;
+            if (!mValidUntil || vsync.ns() < mValidUntil->ns() - threshold) {
+                // if mValidUntil is absent then timeline is not frozen and
+                // vsync should be unique to that timeline.
+                return VsyncOnTimeline::Unique;
+            }
+            if (vsync.ns() > mValidUntil->ns() + threshold) {
+                return VsyncOnTimeline::Outside;
+            }
+            return VsyncOnTimeline::Shared;
+        }
+
     private:
         nsecs_t snapToVsyncAlignedWithRenderRate(Model model, nsecs_t vsync);
         VsyncSequence getVsyncSequenceLocked(Model, nsecs_t vsync);
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index 5246fe2..d772780 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -3855,7 +3855,8 @@
         ftl::FakeGuard guard(kMainThreadContext);
 
         // For hotplug reconnect, renew the registration since display modes have been reloaded.
-        mScheduler->registerDisplay(display->getPhysicalId(), display->holdRefreshRateSelector());
+        mScheduler->registerDisplay(display->getPhysicalId(), display->holdRefreshRateSelector(),
+                                    mActiveDisplayId);
     }
 
     if (display->isVirtual()) {
@@ -3894,7 +3895,7 @@
         if (display->isVirtual()) {
             releaseVirtualDisplay(display->getVirtualId());
         } else {
-            mScheduler->unregisterDisplay(display->getPhysicalId());
+            mScheduler->unregisterDisplay(display->getPhysicalId(), mActiveDisplayId);
         }
     }
 
@@ -4506,7 +4507,8 @@
                                              getFactory(), activeRefreshRate, *mTimeStats);
 
     // The pacesetter must be registered before EventThread creation below.
-    mScheduler->registerDisplay(display->getPhysicalId(), display->holdRefreshRateSelector());
+    mScheduler->registerDisplay(display->getPhysicalId(), display->holdRefreshRateSelector(),
+                                mActiveDisplayId);
     if (FlagManager::getInstance().vrr_config()) {
         mScheduler->setRenderRate(display->getPhysicalId(), activeMode.fps,
                                   /*applyImmediately*/ true);
diff --git a/services/surfaceflinger/tests/unittests/SchedulerTest.cpp b/services/surfaceflinger/tests/unittests/SchedulerTest.cpp
index 4fb0690..fc54a8b 100644
--- a/services/surfaceflinger/tests/unittests/SchedulerTest.cpp
+++ b/services/surfaceflinger/tests/unittests/SchedulerTest.cpp
@@ -343,12 +343,15 @@
 }
 
 TEST_F(SchedulerTest, chooseDisplayModesMultipleDisplays) {
+    constexpr PhysicalDisplayId kActiveDisplayId = kDisplayId1;
     mScheduler->registerDisplay(kDisplayId1,
                                 std::make_shared<RefreshRateSelector>(kDisplay1Modes,
-                                                                      kDisplay1Mode60->getId()));
+                                                                      kDisplay1Mode60->getId()),
+                                kActiveDisplayId);
     mScheduler->registerDisplay(kDisplayId2,
                                 std::make_shared<RefreshRateSelector>(kDisplay2Modes,
-                                                                      kDisplay2Mode60->getId()));
+                                                                      kDisplay2Mode60->getId()),
+                                kActiveDisplayId);
 
     mScheduler->setDisplayPowerMode(kDisplayId1, hal::PowerMode::ON);
     mScheduler->setDisplayPowerMode(kDisplayId2, hal::PowerMode::ON);
@@ -411,10 +414,10 @@
     {
         // The kDisplayId3 does not support 120Hz, The pacesetter display rate is chosen to be 120
         // Hz. In this case only the display kDisplayId3 choose 60Hz as it does not support 120Hz.
-        mScheduler
-                ->registerDisplay(kDisplayId3,
-                                  std::make_shared<RefreshRateSelector>(kDisplay3Modes,
-                                                                        kDisplay3Mode60->getId()));
+        mScheduler->registerDisplay(kDisplayId3,
+                                    std::make_shared<RefreshRateSelector>(kDisplay3Modes,
+                                                                          kDisplay3Mode60->getId()),
+                                    kActiveDisplayId);
         mScheduler->setDisplayPowerMode(kDisplayId3, hal::PowerMode::ON);
 
         const GlobalSignals globalSignals = {.touch = true};
@@ -457,12 +460,15 @@
 }
 
 TEST_F(SchedulerTest, onFrameSignalMultipleDisplays) {
+    constexpr PhysicalDisplayId kActiveDisplayId = kDisplayId1;
     mScheduler->registerDisplay(kDisplayId1,
                                 std::make_shared<RefreshRateSelector>(kDisplay1Modes,
-                                                                      kDisplay1Mode60->getId()));
+                                                                      kDisplay1Mode60->getId()),
+                                kActiveDisplayId);
     mScheduler->registerDisplay(kDisplayId2,
                                 std::make_shared<RefreshRateSelector>(kDisplay2Modes,
-                                                                      kDisplay2Mode60->getId()));
+                                                                      kDisplay2Mode60->getId()),
+                                kActiveDisplayId);
 
     using VsyncIds = std::vector<std::pair<PhysicalDisplayId, VsyncId>>;
 
@@ -585,7 +591,8 @@
                                 mFlinger.getTimeStats(),
                                 mSchedulerCallback};
 
-    scheduler.registerDisplay(kMode->getPhysicalDisplayId(), vrrSelectorPtr, vrrTracker);
+    scheduler.registerDisplay(kMode->getPhysicalDisplayId(), vrrSelectorPtr, std::nullopt,
+                              vrrTracker);
     vrrSelectorPtr->setActiveMode(kMode->getId(), frameRate);
     scheduler.setRenderRate(kMode->getPhysicalDisplayId(), frameRate, /*applyImmediately*/ false);
     vrrTracker->addVsyncTimestamp(0);
diff --git a/services/surfaceflinger/tests/unittests/TestableScheduler.h b/services/surfaceflinger/tests/unittests/TestableScheduler.h
index 198a5de..f063809 100644
--- a/services/surfaceflinger/tests/unittests/TestableScheduler.h
+++ b/services/surfaceflinger/tests/unittests/TestableScheduler.h
@@ -53,7 +53,7 @@
                       factory, selectorPtr->getActiveMode().fps, timeStats) {
         const auto displayId = selectorPtr->getActiveMode().modePtr->getPhysicalDisplayId();
         registerDisplay(displayId, std::move(selectorPtr), std::move(controller),
-                        std::move(tracker));
+                        std::move(tracker), displayId);
 
         ON_CALL(*this, postMessage).WillByDefault([](sp<MessageHandler>&& handler) {
             // Execute task to prevent broken promise exception on destruction.
@@ -85,14 +85,16 @@
 
     void registerDisplay(
             PhysicalDisplayId displayId, RefreshRateSelectorPtr selectorPtr,
+            std::optional<PhysicalDisplayId> activeDisplayIdOpt = {},
             std::shared_ptr<VSyncTracker> vsyncTracker = std::make_shared<mock::VSyncTracker>()) {
         registerDisplay(displayId, std::move(selectorPtr),
-                        std::make_unique<mock::VsyncController>(), vsyncTracker);
+                        std::make_unique<mock::VsyncController>(), vsyncTracker,
+                        activeDisplayIdOpt.value_or(displayId));
     }
 
     void registerDisplay(PhysicalDisplayId displayId, RefreshRateSelectorPtr selectorPtr,
                          std::unique_ptr<VsyncController> controller,
-                         std::shared_ptr<VSyncTracker> tracker) {
+                         std::shared_ptr<VSyncTracker> tracker, PhysicalDisplayId activeDisplayId) {
         ftl::FakeGuard guard(kMainThreadContext);
         Scheduler::registerDisplayInternal(displayId, std::move(selectorPtr),
                                            std::shared_ptr<VsyncSchedule>(
@@ -101,16 +103,12 @@
                                                                              mock::VSyncDispatch>(),
                                                                      std::move(controller),
                                                                      mockRequestHardwareVsync
-                                                                             .AsStdFunction())));
+                                                                             .AsStdFunction())),
+                                           activeDisplayId);
     }
 
     testing::MockFunction<void(PhysicalDisplayId, bool)> mockRequestHardwareVsync;
 
-    void unregisterDisplay(PhysicalDisplayId displayId) {
-        ftl::FakeGuard guard(kMainThreadContext);
-        Scheduler::unregisterDisplay(displayId);
-    }
-
     void setDisplayPowerMode(PhysicalDisplayId displayId, hal::PowerMode powerMode) {
         ftl::FakeGuard guard(kMainThreadContext);
         Scheduler::setDisplayPowerMode(displayId, powerMode);
diff --git a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
index 007383b..4197cbd 100644
--- a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
+++ b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
@@ -1118,8 +1118,8 @@
                 if (mFlinger.scheduler() && mSchedulerRegistration) {
                     mFlinger.scheduler()->registerDisplay(*physicalId,
                                                           mCreationArgs.refreshRateSelector,
-                                                          std::move(controller),
-                                                          std::move(tracker));
+                                                          std::move(controller), std::move(tracker),
+                                                          mFlinger.mutableActiveDisplayId());
                 }
             }
 
diff --git a/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp b/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp
index 5109ea6..f36a8a6 100644
--- a/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp
+++ b/services/surfaceflinger/tests/unittests/VSyncPredictorTest.cpp
@@ -673,6 +673,36 @@
     EXPECT_THAT(tracker.nextAnticipatedVSyncTimeFrom(mNow + 5100), Eq(mNow + 6 * mPeriod));
 }
 
+TEST_F(VSyncPredictorTest, setRenderRateWhenRenderRateGoesDown) {
+    SET_FLAG_FOR_TEST(flags::vrr_config, true);
+    SET_FLAG_FOR_TEST(flags::vrr_bugfix_24q4, true);
+
+    const int32_t kGroup = 0;
+    const auto kResolution = ui::Size(1920, 1080);
+    const auto vsyncRate = Fps::fromPeriodNsecs(500);
+    const auto minFrameRate = Fps::fromPeriodNsecs(1000);
+    hal::VrrConfig vrrConfig;
+    vrrConfig.minFrameIntervalNs = minFrameRate.getPeriodNsecs();
+    const ftl::NonNull<DisplayModePtr> kMode =
+            ftl::as_non_null(createDisplayModeBuilder(DisplayModeId(0), vsyncRate, kGroup,
+                                                      kResolution, DEFAULT_DISPLAY_ID)
+                                     .setVrrConfig(std::move(vrrConfig))
+                                     .build());
+
+    VSyncPredictor vrrTracker{std::make_unique<ClockWrapper>(mClock), kMode, kHistorySize,
+                              kMinimumSamplesForPrediction, kOutlierTolerancePercent};
+
+    Fps frameRate = Fps::fromPeriodNsecs(1000);
+    vrrTracker.setRenderRate(frameRate, /*applyImmediately*/ false);
+    vrrTracker.addVsyncTimestamp(0);
+    EXPECT_EQ(1000, vrrTracker.nextAnticipatedVSyncTimeFrom(700));
+    EXPECT_EQ(2000, vrrTracker.nextAnticipatedVSyncTimeFrom(1000, 1000));
+
+    frameRate = Fps::fromPeriodNsecs(3000);
+    vrrTracker.setRenderRate(frameRate, /*applyImmediately*/ false);
+    EXPECT_TRUE(vrrTracker.isVSyncInPhase(2000, frameRate));
+}
+
 TEST_F(VSyncPredictorTest, setRenderRateHighIsAppliedImmediately) {
     SET_FLAG_FOR_TEST(flags::vrr_config, true);