Merge "SF: Add VsyncSchedule skeleton"
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index f054596..6d837c2 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -6,6 +6,7 @@
 # Only turn on clang-format check for the following subfolders.
 clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
                cmds/idlcli/
+               cmds/installd/
                cmds/servicemanager/
                include/input/
                include/powermanager/
diff --git a/cmds/installd/InstalldNativeService.cpp b/cmds/installd/InstalldNativeService.cpp
index 8d23efc..5472da7 100644
--- a/cmds/installd/InstalldNativeService.cpp
+++ b/cmds/installd/InstalldNativeService.cpp
@@ -637,7 +637,6 @@
         const std::vector<android::os::CreateAppDataArgs>& args,
         std::vector<android::os::CreateAppDataResult>* _aidl_return) {
     ENFORCE_UID(AID_SYSTEM);
-    std::lock_guard<std::recursive_mutex> lock(mLock);
 
     std::vector<android::os::CreateAppDataResult> results;
     for (const auto &arg : args) {
@@ -1621,7 +1620,6 @@
         const std::string& instructionSet) {
     ENFORCE_UID(AID_SYSTEM);
     CHECK_ARGUMENT_PATH(codePath);
-    std::lock_guard<std::recursive_mutex> lock(mLock);
 
     char dex_path[PKG_PATH_MAX];
 
@@ -3007,8 +3005,9 @@
 
     const char* uuid_ = uuid->c_str();
 
+    std::lock_guard<std::recursive_mutex> lock(mMountsLock);
+
     std::string mirrorVolCePath(StringPrintf("%s/%s", kDataMirrorCePath, uuid_));
-    std::lock_guard<std::recursive_mutex> lock(mLock);
     if (fs_prepare_dir(mirrorVolCePath.c_str(), 0711, AID_SYSTEM, AID_SYSTEM) != 0) {
         return error("Failed to create CE mirror");
     }
@@ -3077,8 +3076,9 @@
     std::string mirrorCeVolPath(StringPrintf("%s/%s", kDataMirrorCePath, uuid_));
     std::string mirrorDeVolPath(StringPrintf("%s/%s", kDataMirrorDePath, uuid_));
 
+    std::lock_guard<std::recursive_mutex> lock(mMountsLock);
+
     // Unmount CE storage
-    std::lock_guard<std::recursive_mutex> lock(mLock);
     if (TEMP_FAILURE_RETRY(umount(mirrorCeVolPath.c_str())) != 0) {
         if (errno != ENOENT) {
             res = error(StringPrintf("Failed to umount %s %s", mirrorCeVolPath.c_str(),
diff --git a/cmds/installd/dexopt.cpp b/cmds/installd/dexopt.cpp
index f3ec63f..d8fcc57 100644
--- a/cmds/installd/dexopt.cpp
+++ b/cmds/installd/dexopt.cpp
@@ -459,8 +459,8 @@
     });
 }
 
-static unique_fd open_spnashot_profile(uid_t uid, const std::string& package_name,
-        const std::string& location) {
+static unique_fd open_snapshot_profile(uid_t uid, const std::string& package_name,
+                                       const std::string& location) {
     std::string profile = create_snapshot_profile_path(package_name, location);
     return open_profile(uid, profile, O_CREAT | O_RDWR | O_TRUNC,  S_IRUSR | S_IWUSR);
 }
@@ -2562,7 +2562,7 @@
                                         const std::string& classpath) {
     int app_shared_gid = multiuser_get_shared_gid(/*user_id*/ 0, app_id);
 
-    unique_fd snapshot_fd = open_spnashot_profile(AID_SYSTEM, package_name, profile_name);
+    unique_fd snapshot_fd = open_snapshot_profile(AID_SYSTEM, package_name, profile_name);
     if (snapshot_fd < 0) {
         return false;
     }
@@ -2636,7 +2636,7 @@
     }
 
     // Open and create the snapshot profile.
-    unique_fd snapshot_fd = open_spnashot_profile(AID_SYSTEM, package_name, profile_name);
+    unique_fd snapshot_fd = open_snapshot_profile(AID_SYSTEM, package_name, profile_name);
 
     // Collect all non empty profiles.
     // The collection will traverse all applications profiles and find the non empty files.
diff --git a/include/input/Input.h b/include/input/Input.h
index 1c79c4a..1e06257 100644
--- a/include/input/Input.h
+++ b/include/input/Input.h
@@ -377,6 +377,7 @@
     // window scale. The global scale will be applied to TOUCH/TOOL_MAJOR/MINOR
     // axes, however the window scaling will not.
     void scale(float globalScale, float windowXScale, float windowYScale);
+    void applyOffset(float xOffset, float yOffset);
 
     void transform(const ui::Transform& transform);
 
@@ -566,7 +567,7 @@
 
     inline float getYOffset() const { return mTransform.ty(); }
 
-    inline const ui::Transform& getTransform() const { return mTransform; }
+    inline ui::Transform getTransform() const { return mTransform; }
 
     inline float getXPrecision() const { return mXPrecision; }
 
@@ -582,7 +583,7 @@
 
     void setCursorPosition(float x, float y);
 
-    inline const ui::Transform& getRawTransform() const { return mRawTransform; }
+    ui::Transform getRawTransform() const { return mRawTransform; }
 
     static inline bool isValidCursorPosition(float x, float y) { return !isnan(x) && !isnan(y); }
 
diff --git a/libs/binder/ndk/include_cpp/android/binder_auto_utils.h b/libs/binder/ndk/include_cpp/android/binder_auto_utils.h
index 0ad400b..c903998 100644
--- a/libs/binder/ndk/include_cpp/android/binder_auto_utils.h
+++ b/libs/binder/ndk/include_cpp/android/binder_auto_utils.h
@@ -365,6 +365,8 @@
     ScopedFileDescriptor(ScopedFileDescriptor&&) = default;
     ScopedFileDescriptor& operator=(ScopedFileDescriptor&&) = default;
 
+    ScopedFileDescriptor dup() const { return ScopedFileDescriptor(::dup(get())); }
+
     bool operator!=(const ScopedFileDescriptor& rhs) const { return get() != rhs.get(); }
     bool operator<(const ScopedFileDescriptor& rhs) const { return get() < rhs.get(); }
     bool operator<=(const ScopedFileDescriptor& rhs) const { return get() <= rhs.get(); }
diff --git a/libs/input/Input.cpp b/libs/input/Input.cpp
index 69ea7df..8974b22 100644
--- a/libs/input/Input.cpp
+++ b/libs/input/Input.cpp
@@ -343,6 +343,11 @@
     scaleAxisValue(*this, AMOTION_EVENT_AXIS_RELATIVE_Y, windowYScale);
 }
 
+void PointerCoords::applyOffset(float xOffset, float yOffset) {
+    setAxisValue(AMOTION_EVENT_AXIS_X, getX() + xOffset);
+    setAxisValue(AMOTION_EVENT_AXIS_Y, getY() + yOffset);
+}
+
 #ifdef __linux__
 status_t PointerCoords::readFromParcel(Parcel* parcel) {
     bits = parcel->readInt64();
diff --git a/services/inputflinger/dispatcher/Connection.cpp b/services/inputflinger/dispatcher/Connection.cpp
index cee9c39..b4497fd 100644
--- a/services/inputflinger/dispatcher/Connection.cpp
+++ b/services/inputflinger/dispatcher/Connection.cpp
@@ -22,7 +22,7 @@
 
 Connection::Connection(const std::shared_ptr<InputChannel>& inputChannel, bool monitor,
                        const IdGenerator& idGenerator)
-      : status(STATUS_NORMAL),
+      : status(Status::NORMAL),
         inputChannel(inputChannel),
         monitor(monitor),
         inputPublisher(inputChannel),
@@ -40,19 +40,6 @@
     return "?";
 }
 
-const char* Connection::getStatusLabel() const {
-    switch (status) {
-        case STATUS_NORMAL:
-            return "NORMAL";
-        case STATUS_BROKEN:
-            return "BROKEN";
-        case STATUS_ZOMBIE:
-            return "ZOMBIE";
-        default:
-            return "UNKNOWN";
-    }
-}
-
 std::deque<DispatchEntry*>::iterator Connection::findWaitQueueEntry(uint32_t seq) {
     for (std::deque<DispatchEntry*>::iterator it = waitQueue.begin(); it != waitQueue.end(); it++) {
         if ((*it)->seq == seq) {
diff --git a/services/inputflinger/dispatcher/Connection.h b/services/inputflinger/dispatcher/Connection.h
index ba60283..dc6a081 100644
--- a/services/inputflinger/dispatcher/Connection.h
+++ b/services/inputflinger/dispatcher/Connection.h
@@ -33,13 +33,16 @@
     virtual ~Connection();
 
 public:
-    enum Status {
+    enum class Status {
         // Everything is peachy.
-        STATUS_NORMAL,
+        NORMAL,
         // An unrecoverable communication error has occurred.
-        STATUS_BROKEN,
+        BROKEN,
         // The input channel has been unregistered.
-        STATUS_ZOMBIE
+        ZOMBIE,
+
+        ftl_first = NORMAL,
+        ftl_last = ZOMBIE,
     };
 
     Status status;
@@ -66,7 +69,6 @@
     inline const std::string getInputChannelName() const { return inputChannel->getName(); }
 
     const std::string getWindowName() const;
-    const char* getStatusLabel() const;
 
     std::deque<DispatchEntry*>::iterator findWaitQueueEntry(uint32_t seq);
 };
diff --git a/services/inputflinger/dispatcher/Entry.cpp b/services/inputflinger/dispatcher/Entry.cpp
index 3d0818b..f6bb6a6 100644
--- a/services/inputflinger/dispatcher/Entry.cpp
+++ b/services/inputflinger/dispatcher/Entry.cpp
@@ -214,7 +214,7 @@
                          int32_t edgeFlags, float xPrecision, float yPrecision,
                          float xCursorPosition, float yCursorPosition, nsecs_t downTime,
                          uint32_t pointerCount, const PointerProperties* pointerProperties,
-                         const PointerCoords* pointerCoords)
+                         const PointerCoords* pointerCoords, float xOffset, float yOffset)
       : EventEntry(id, Type::MOTION, eventTime, policyFlags),
         deviceId(deviceId),
         source(source),
@@ -235,6 +235,9 @@
     for (uint32_t i = 0; i < pointerCount; i++) {
         this->pointerProperties[i].copyFrom(pointerProperties[i]);
         this->pointerCoords[i].copyFrom(pointerCoords[i]);
+        if (xOffset || yOffset) {
+            this->pointerCoords[i].applyOffset(xOffset, yOffset);
+        }
     }
 }
 
@@ -286,11 +289,10 @@
 
 std::string SensorEntry::getDescription() const {
     std::string msg;
-    std::string sensorTypeStr(ftl::enum_name(sensorType).value_or("?"));
     msg += StringPrintf("SensorEntry(deviceId=%d, source=%s, sensorType=%s, "
                         "accuracy=0x%08x, hwTimestamp=%" PRId64,
-                        deviceId, inputEventSourceToString(source).c_str(), sensorTypeStr.c_str(),
-                        accuracy, hwTimestamp);
+                        deviceId, inputEventSourceToString(source).c_str(),
+                        ftl::enum_string(sensorType).c_str(), accuracy, hwTimestamp);
 
     if (!GetBoolProperty("ro.debuggable", false)) {
         for (size_t i = 0; i < values.size(); i++) {
diff --git a/services/inputflinger/dispatcher/Entry.h b/services/inputflinger/dispatcher/Entry.h
index 0f79296..477781a 100644
--- a/services/inputflinger/dispatcher/Entry.h
+++ b/services/inputflinger/dispatcher/Entry.h
@@ -184,7 +184,8 @@
                 int32_t metaState, int32_t buttonState, MotionClassification classification,
                 int32_t edgeFlags, float xPrecision, float yPrecision, float xCursorPosition,
                 float yCursorPosition, nsecs_t downTime, uint32_t pointerCount,
-                const PointerProperties* pointerProperties, const PointerCoords* pointerCoords);
+                const PointerProperties* pointerProperties, const PointerCoords* pointerCoords,
+                float xOffset, float yOffset);
     std::string getDescription() const override;
 
     ~MotionEntry() override;
diff --git a/services/inputflinger/dispatcher/InputDispatcher.cpp b/services/inputflinger/dispatcher/InputDispatcher.cpp
index 04ff599..d5d906b 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.cpp
+++ b/services/inputflinger/dispatcher/InputDispatcher.cpp
@@ -378,7 +378,7 @@
                                           motionEntry.yPrecision, motionEntry.xCursorPosition,
                                           motionEntry.yCursorPosition, motionEntry.downTime,
                                           motionEntry.pointerCount, motionEntry.pointerProperties,
-                                          pointerCoords.data());
+                                          pointerCoords.data(), 0 /* xOffset */, 0 /* yOffset */);
 
     if (motionEntry.injectionState) {
         combinedMotionEntry->injectionState = motionEntry.injectionState;
@@ -506,6 +506,12 @@
     return true;
 }
 
+vec2 transformWithoutTranslation(const ui::Transform& transform, float x, float y) {
+    const vec2 transformedXy = transform.transform(x, y);
+    const vec2 transformedOrigin = transform.transform(0, 0);
+    return transformedXy - transformedOrigin;
+}
+
 // Returns true if the event type passed as argument represents a user activity.
 bool isUserActivityEvent(const EventEntry& eventEntry) {
     switch (eventEntry.type) {
@@ -1743,7 +1749,7 @@
     // pile up.
     ALOGW("Canceling events for %s because it is unresponsive",
           connection->inputChannel->getName().c_str());
-    if (connection->status == Connection::STATUS_NORMAL) {
+    if (connection->status == Connection::Status::NORMAL) {
         CancelationOptions options(CancelationOptions::CANCEL_ALL_EVENTS,
                                    "application not responding");
         synthesizeCancelationEventsForConnectionLocked(connection, options);
@@ -2819,10 +2825,11 @@
 
     // Skip this event if the connection status is not normal.
     // We don't want to enqueue additional outbound events if the connection is broken.
-    if (connection->status != Connection::STATUS_NORMAL) {
+    if (connection->status != Connection::Status::NORMAL) {
         if (DEBUG_DISPATCH_CYCLE) {
             ALOGD("channel '%s' ~ Dropping event because the channel status is %s",
-                  connection->getInputChannelName().c_str(), connection->getStatusLabel());
+                  connection->getInputChannelName().c_str(),
+                  ftl::enum_string(connection->status).c_str());
         }
         return;
     }
@@ -3136,7 +3143,7 @@
         ALOGD("channel '%s' ~ startDispatchCycle", connection->getInputChannelName().c_str());
     }
 
-    while (connection->status == Connection::STATUS_NORMAL && !connection->outboundQueue.empty()) {
+    while (connection->status == Connection::Status::NORMAL && !connection->outboundQueue.empty()) {
         DispatchEntry* dispatchEntry = connection->outboundQueue.front();
         dispatchEntry->deliveryTime = currentTime;
         const std::chrono::nanoseconds timeout =
@@ -3357,8 +3364,8 @@
               connection->getInputChannelName().c_str(), seq, toString(handled));
     }
 
-    if (connection->status == Connection::STATUS_BROKEN ||
-        connection->status == Connection::STATUS_ZOMBIE) {
+    if (connection->status == Connection::Status::BROKEN ||
+        connection->status == Connection::Status::ZOMBIE) {
         return;
     }
 
@@ -3385,8 +3392,8 @@
 
     // The connection appears to be unrecoverably broken.
     // Ignore already broken or zombie connections.
-    if (connection->status == Connection::STATUS_NORMAL) {
-        connection->status = Connection::STATUS_BROKEN;
+    if (connection->status == Connection::Status::NORMAL) {
+        connection->status = Connection::Status::BROKEN;
 
         if (notify) {
             // Notify other system components.
@@ -3394,7 +3401,6 @@
                   connection->getInputChannelName().c_str());
 
             auto command = [this, connection]() REQUIRES(mLock) {
-                if (connection->status == Connection::STATUS_ZOMBIE) return;
                 scoped_unlock unlock(mLock);
                 mPolicy->notifyInputChannelBroken(connection->inputChannel->getConnectionToken());
             };
@@ -3530,7 +3536,7 @@
 
 void InputDispatcher::synthesizeCancelationEventsForConnectionLocked(
         const sp<Connection>& connection, const CancelationOptions& options) {
-    if (connection->status == Connection::STATUS_BROKEN) {
+    if (connection->status == Connection::Status::BROKEN) {
         return;
     }
 
@@ -3603,7 +3609,7 @@
 
 void InputDispatcher::synthesizePointerDownEventsForConnectionLocked(
         const sp<Connection>& connection) {
-    if (connection->status == Connection::STATUS_BROKEN) {
+    if (connection->status == Connection::Status::BROKEN) {
         return;
     }
 
@@ -3752,7 +3758,7 @@
                                           originalMotionEntry.xCursorPosition,
                                           originalMotionEntry.yCursorPosition,
                                           originalMotionEntry.downTime, splitPointerCount,
-                                          splitPointerProperties, splitPointerCoords);
+                                          splitPointerProperties, splitPointerCoords, 0, 0);
 
     if (originalMotionEntry.injectionState) {
         splitMotionEntry->injectionState = originalMotionEntry.injectionState;
@@ -3978,7 +3984,7 @@
                                               args->xPrecision, args->yPrecision,
                                               args->xCursorPosition, args->yCursorPosition,
                                               args->downTime, args->pointerCount,
-                                              args->pointerProperties, args->pointerCoords);
+                                              args->pointerProperties, args->pointerCoords, 0, 0);
 
         if (args->id != android::os::IInputConstants::INVALID_INPUT_EVENT_ID &&
             IdGenerator::getSource(args->id) == IdGenerator::Source::INPUT_READER &&
@@ -4208,8 +4214,10 @@
                                                   motionEvent.getRawXCursorPosition(),
                                                   motionEvent.getRawYCursorPosition(),
                                                   motionEvent.getDownTime(), uint32_t(pointerCount),
-                                                  pointerProperties, samplePointerCoords);
-            transformMotionEntryForInjectionLocked(*injectedEntry, motionEvent.getTransform());
+                                                  pointerProperties, samplePointerCoords,
+                                                  motionEvent.getXOffset(),
+                                                  motionEvent.getYOffset());
+            transformMotionEntryForInjectionLocked(*injectedEntry);
             injectedEntries.push(std::move(injectedEntry));
             for (size_t i = motionEvent.getHistorySize(); i > 0; i--) {
                 sampleEventTimes += 1;
@@ -4228,9 +4236,9 @@
                                                       motionEvent.getRawYCursorPosition(),
                                                       motionEvent.getDownTime(),
                                                       uint32_t(pointerCount), pointerProperties,
-                                                      samplePointerCoords);
-                transformMotionEntryForInjectionLocked(*nextInjectedEntry,
-                                                       motionEvent.getTransform());
+                                                      samplePointerCoords, motionEvent.getXOffset(),
+                                                      motionEvent.getYOffset());
+                transformMotionEntryForInjectionLocked(*nextInjectedEntry);
                 injectedEntries.push(std::move(nextInjectedEntry));
             }
             break;
@@ -4394,28 +4402,35 @@
     }
 }
 
-void InputDispatcher::transformMotionEntryForInjectionLocked(
-        MotionEntry& entry, const ui::Transform& injectedTransform) const {
+void InputDispatcher::transformMotionEntryForInjectionLocked(MotionEntry& entry) const {
+    const bool isRelativeMouseEvent = isFromSource(entry.source, AINPUT_SOURCE_MOUSE_RELATIVE);
+    if (!isRelativeMouseEvent && !isFromSource(entry.source, AINPUT_SOURCE_CLASS_POINTER)) {
+        return;
+    }
+
     // Input injection works in the logical display coordinate space, but the input pipeline works
     // display space, so we need to transform the injected events accordingly.
     const auto it = mDisplayInfos.find(entry.displayId);
     if (it == mDisplayInfos.end()) return;
-    const auto& transformToDisplay = it->second.transform.inverse() * injectedTransform;
+    const auto& transformToDisplay = it->second.transform.inverse();
 
     for (uint32_t i = 0; i < entry.pointerCount; i++) {
         PointerCoords& pc = entry.pointerCoords[i];
-        // Make a copy of the injected coords. We cannot change them in place because some of them
-        // are interdependent (for example, X coordinate might depend on the Y coordinate).
-        PointerCoords injectedCoords = entry.pointerCoords[i];
+        const auto xy = isRelativeMouseEvent
+                ? transformWithoutTranslation(transformToDisplay, pc.getX(), pc.getY())
+                : transformToDisplay.transform(pc.getXYValue());
+        pc.setAxisValue(AMOTION_EVENT_AXIS_X, xy.x);
+        pc.setAxisValue(AMOTION_EVENT_AXIS_Y, xy.y);
 
-        BitSet64 bits(injectedCoords.bits);
-        while (!bits.isEmpty()) {
-            const auto axis = static_cast<int32_t>(bits.clearFirstMarkedBit());
-            const float value =
-                    MotionEvent::calculateTransformedAxisValue(axis, entry.source,
-                                                               transformToDisplay, injectedCoords);
-            pc.setAxisValue(axis, value);
-        }
+        // Axes with relative values never represent points on a screen, so they should never have
+        // translation applied. If a device does not report relative values, these values are always
+        // 0, and will remain unaffected by the following operation.
+        const auto rel =
+                transformWithoutTranslation(transformToDisplay,
+                                            pc.getAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X),
+                                            pc.getAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y));
+        pc.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_X, rel.x);
+        pc.setAxisValue(AMOTION_EVENT_AXIS_RELATIVE_Y, rel.y);
     }
 }
 
@@ -4674,8 +4689,10 @@
                         if (wallpaper != nullptr) {
                             sp<Connection> wallpaperConnection =
                                     getConnectionLocked(wallpaper->getToken());
-                            synthesizeCancelationEventsForConnectionLocked(wallpaperConnection,
-                                                                           options);
+                            if (wallpaperConnection != nullptr) {
+                                synthesizeCancelationEventsForConnectionLocked(wallpaperConnection,
+                                                                               options);
+                            }
                         }
                     }
                 }
@@ -5271,7 +5288,8 @@
                                          "status=%s, monitor=%s, responsive=%s\n",
                                  connection->inputChannel->getFd().get(),
                                  connection->getInputChannelName().c_str(),
-                                 connection->getWindowName().c_str(), connection->getStatusLabel(),
+                                 connection->getWindowName().c_str(),
+                                 ftl::enum_string(connection->status).c_str(),
                                  toString(connection->monitor), toString(connection->responsive));
 
             if (!connection->outboundQueue.empty()) {
@@ -5444,7 +5462,7 @@
     nsecs_t currentTime = now();
     abortBrokenDispatchCycleLocked(currentTime, connection, notify);
 
-    connection->status = Connection::STATUS_ZOMBIE;
+    connection->status = Connection::Status::ZOMBIE;
     return OK;
 }
 
@@ -5663,7 +5681,7 @@
             }
         }
         traceWaitQueueLength(*connection);
-        if (restartEvent && connection->status == Connection::STATUS_NORMAL) {
+        if (restartEvent && connection->status == Connection::Status::NORMAL) {
             connection->outboundQueue.push_front(dispatchEntry);
             traceOutboundQueueLength(*connection);
         } else {
@@ -5961,7 +5979,7 @@
 
         mLock.lock();
 
-        if (connection->status != Connection::STATUS_NORMAL) {
+        if (connection->status != Connection::Status::NORMAL) {
             connection->inputState.removeFallbackKey(originalKeyCode);
             return false;
         }
diff --git a/services/inputflinger/dispatcher/InputDispatcher.h b/services/inputflinger/dispatcher/InputDispatcher.h
index 6f05670..8a551cf 100644
--- a/services/inputflinger/dispatcher/InputDispatcher.h
+++ b/services/inputflinger/dispatcher/InputDispatcher.h
@@ -280,9 +280,7 @@
     bool hasInjectionPermission(int32_t injectorPid, int32_t injectorUid);
     void setInjectionResult(EventEntry& entry,
                             android::os::InputEventInjectionResult injectionResult);
-    void transformMotionEntryForInjectionLocked(MotionEntry&,
-                                                const ui::Transform& injectedTransform) const
-            REQUIRES(mLock);
+    void transformMotionEntryForInjectionLocked(MotionEntry&) const REQUIRES(mLock);
 
     std::condition_variable mInjectionSyncFinished;
     void incrementPendingForegroundDispatches(EventEntry& entry);
diff --git a/services/inputflinger/dispatcher/InputState.cpp b/services/inputflinger/dispatcher/InputState.cpp
index ad3c615..3bb0bc9 100644
--- a/services/inputflinger/dispatcher/InputState.cpp
+++ b/services/inputflinger/dispatcher/InputState.cpp
@@ -296,7 +296,8 @@
                                                   memento.yPrecision, memento.xCursorPosition,
                                                   memento.yCursorPosition, memento.downTime,
                                                   memento.pointerCount, memento.pointerProperties,
-                                                  memento.pointerCoords));
+                                                  memento.pointerCoords, 0 /*xOffset*/,
+                                                  0 /*yOffset*/));
         }
     }
     return events;
@@ -348,7 +349,8 @@
                                                   AMOTION_EVENT_EDGE_FLAG_NONE, memento.xPrecision,
                                                   memento.yPrecision, memento.xCursorPosition,
                                                   memento.yCursorPosition, memento.downTime,
-                                                  pointerCount, pointerProperties, pointerCoords));
+                                                  pointerCount, pointerProperties, pointerCoords,
+                                                  0 /*xOffset*/, 0 /*yOffset*/));
         }
 
         memento.firstNewPointerIdx = INVALID_POINTER_INDEX;
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index eaea4e2..ffacfb1 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -219,21 +219,21 @@
     template <class T>
     T getAnrTokenLockedInterruptible(std::chrono::nanoseconds timeout, std::queue<T>& storage,
                                      std::unique_lock<std::mutex>& lock) REQUIRES(mLock) {
-        const std::chrono::time_point start = std::chrono::steady_clock::now();
-        std::chrono::duration timeToWait = timeout + 100ms; // provide some slack
-
         // If there is an ANR, Dispatcher won't be idle because there are still events
         // in the waitQueue that we need to check on. So we can't wait for dispatcher to be idle
         // before checking if ANR was called.
         // Since dispatcher is not guaranteed to call notifyNoFocusedWindowAnr right away, we need
         // to provide it some time to act. 100ms seems reasonable.
-        mNotifyAnr.wait_for(lock, timeToWait,
-                            [&storage]() REQUIRES(mLock) { return !storage.empty(); });
-        const std::chrono::duration waited = std::chrono::steady_clock::now() - start;
-        if (storage.empty()) {
+        std::chrono::duration timeToWait = timeout + 100ms; // provide some slack
+        const std::chrono::time_point start = std::chrono::steady_clock::now();
+        std::optional<T> token =
+                getItemFromStorageLockedInterruptible(timeToWait, storage, lock, mNotifyAnr);
+        if (!token.has_value()) {
             ADD_FAILURE() << "Did not receive the ANR callback";
             return {};
         }
+
+        const std::chrono::duration waited = std::chrono::steady_clock::now() - start;
         // Ensure that the ANR didn't get raised too early. We can't be too strict here because
         // the dispatcher started counting before this function was called
         if (std::chrono::abs(timeout - waited) > 100ms) {
@@ -243,9 +243,24 @@
                           << std::chrono::duration_cast<std::chrono::milliseconds>(waited).count()
                           << "ms instead";
         }
-        T token = storage.front();
+        return *token;
+    }
+
+    template <class T>
+    std::optional<T> getItemFromStorageLockedInterruptible(std::chrono::nanoseconds timeout,
+                                                           std::queue<T>& storage,
+                                                           std::unique_lock<std::mutex>& lock,
+                                                           std::condition_variable& condition)
+            REQUIRES(mLock) {
+        condition.wait_for(lock, timeout,
+                           [&storage]() REQUIRES(mLock) { return !storage.empty(); });
+        if (storage.empty()) {
+            ADD_FAILURE() << "Did not receive the expected callback";
+            return std::nullopt;
+        }
+        T item = storage.front();
         storage.pop();
-        return token;
+        return std::make_optional(item);
     }
 
     void assertNotifyAnrWasNotCalled() {
@@ -303,6 +318,16 @@
         mNotifyDropWindowWasCalled = false;
     }
 
+    void assertNotifyInputChannelBrokenWasCalled(const sp<IBinder>& token) {
+        std::unique_lock lock(mLock);
+        base::ScopedLockAssertion assumeLocked(mLock);
+        std::optional<sp<IBinder>> receivedToken =
+                getItemFromStorageLockedInterruptible(100ms, mBrokenInputChannels, lock,
+                                                      mNotifyInputChannelBroken);
+        ASSERT_TRUE(receivedToken.has_value());
+        ASSERT_EQ(token, *receivedToken);
+    }
+
 private:
     std::mutex mLock;
     std::unique_ptr<InputEvent> mFilteredEvent GUARDED_BY(mLock);
@@ -321,6 +346,8 @@
     std::queue<int32_t> mAnrMonitorPids GUARDED_BY(mLock);
     std::queue<int32_t> mResponsiveMonitorPids GUARDED_BY(mLock);
     std::condition_variable mNotifyAnr;
+    std::queue<sp<IBinder>> mBrokenInputChannels GUARDED_BY(mLock);
+    std::condition_variable mNotifyInputChannelBroken;
 
     sp<IBinder> mDropTargetWindowToken GUARDED_BY(mLock);
     bool mNotifyDropWindowWasCalled GUARDED_BY(mLock) = false;
@@ -361,7 +388,11 @@
         mNotifyAnr.notify_all();
     }
 
-    void notifyInputChannelBroken(const sp<IBinder>&) override {}
+    void notifyInputChannelBroken(const sp<IBinder>& connectionToken) override {
+        std::scoped_lock lock(mLock);
+        mBrokenInputChannels.push(connectionToken);
+        mNotifyInputChannelBroken.notify_all();
+    }
 
     void notifyFocusChanged(const sp<IBinder>&, const sp<IBinder>&) override {}
 
@@ -1175,6 +1206,8 @@
         mInfo.ownerUid = ownerUid;
     }
 
+    void destroyReceiver() { mInputReceiver = nullptr; }
+
 private:
     const std::string mName;
     std::unique_ptr<FakeInputReceiver> mInputReceiver;
@@ -1438,6 +1471,23 @@
     return NotifyPointerCaptureChangedArgs(/* id */ 0, systemTime(SYSTEM_TIME_MONOTONIC), request);
 }
 
+/**
+ * When a window unexpectedly disposes of its input channel, policy should be notified about the
+ * broken channel.
+ */
+TEST_F(InputDispatcherTest, WhenInputChannelBreaks_PolicyIsNotified) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window =
+            new FakeWindowHandle(application, mDispatcher, "Window that breaks its input channel",
+                                 ADISPLAY_ID_DEFAULT);
+
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {window}}});
+
+    // Window closes its channel, but the window remains.
+    window->destroyReceiver();
+    mFakePolicy->assertNotifyInputChannelBrokenWasCalled(window->getInfo()->token);
+}
+
 TEST_F(InputDispatcherTest, SetInputWindow_SingleWindowTouch) {
     std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
     sp<FakeWindowHandle> window =
@@ -1578,6 +1628,53 @@
 }
 
 /**
+ * Same test as WhenForegroundWindowDisappears_WallpaperTouchIsCanceled above,
+ * with the following differences:
+ * After ACTION_DOWN, Wallpaper window hangs up its channel, which forces the dispatcher to
+ * clean up the connection.
+ * This later may crash dispatcher during ACTION_CANCEL synthesis, if the dispatcher is not careful.
+ * Ensure that there's no crash in the dispatcher.
+ */
+TEST_F(InputDispatcherTest, WhenWallpaperDisappears_NoCrash) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> foregroundWindow =
+            new FakeWindowHandle(application, mDispatcher, "Foreground", ADISPLAY_ID_DEFAULT);
+    foregroundWindow->setHasWallpaper(true);
+    sp<FakeWindowHandle> wallpaperWindow =
+            new FakeWindowHandle(application, mDispatcher, "Wallpaper", ADISPLAY_ID_DEFAULT);
+    wallpaperWindow->setType(WindowInfo::Type::WALLPAPER);
+    constexpr int expectedWallpaperFlags =
+            AMOTION_EVENT_FLAG_WINDOW_IS_OBSCURED | AMOTION_EVENT_FLAG_WINDOW_IS_PARTIALLY_OBSCURED;
+
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {foregroundWindow, wallpaperWindow}}});
+    ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
+              injectMotionDown(mDispatcher, AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT,
+                               {100, 200}))
+            << "Inject motion event should return InputEventInjectionResult::SUCCEEDED";
+
+    // Both foreground window and its wallpaper should receive the touch down
+    foregroundWindow->consumeMotionDown();
+    wallpaperWindow->consumeMotionDown(ADISPLAY_ID_DEFAULT, expectedWallpaperFlags);
+
+    ASSERT_EQ(InputEventInjectionResult::SUCCEEDED,
+              injectMotionEvent(mDispatcher, AMOTION_EVENT_ACTION_MOVE, AINPUT_SOURCE_TOUCHSCREEN,
+                                ADISPLAY_ID_DEFAULT, {110, 200}))
+            << "Inject motion event should return InputEventInjectionResult::SUCCEEDED";
+
+    foregroundWindow->consumeMotionMove();
+    wallpaperWindow->consumeMotionMove(ADISPLAY_ID_DEFAULT, expectedWallpaperFlags);
+
+    // Wallpaper closes its channel, but the window remains.
+    wallpaperWindow->destroyReceiver();
+    mFakePolicy->assertNotifyInputChannelBrokenWasCalled(wallpaperWindow->getInfo()->token);
+
+    // Now the foreground window goes away, but the wallpaper stays, even though its channel
+    // is no longer valid.
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {wallpaperWindow}}});
+    foregroundWindow->consumeMotionCancel();
+}
+
+/**
  * A single window that receives touch (on top), and a wallpaper window underneath it.
  * The top window gets a multitouch gesture.
  * Ensure that wallpaper gets the same gesture.
@@ -2079,33 +2176,6 @@
     secondWindow->assertNoEvents();
 }
 
-// Ensure that when a MotionEvent that has a custom transform is injected, the post-transformed
-// event should be treated as being in the logical display space.
-TEST_F(InputDispatcherDisplayProjectionTest, InjectionWithTransformInLogicalDisplaySpace) {
-    auto [firstWindow, secondWindow] = setupScaledDisplayScenario();
-
-    const std::array<float, 9> matrix = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 0.0, 0.0, 1.0};
-    ui::Transform injectedEventTransform;
-    injectedEventTransform.set(matrix);
-    const vec2 expectedPoint{75, 55}; // The injected point in the logical display space.
-    const vec2 untransformedPoint = injectedEventTransform.inverse().transform(expectedPoint);
-
-    MotionEvent event = MotionEventBuilder(AMOTION_EVENT_ACTION_DOWN, AINPUT_SOURCE_TOUCHSCREEN)
-                                .displayId(ADISPLAY_ID_DEFAULT)
-                                .eventTime(systemTime(SYSTEM_TIME_MONOTONIC))
-                                .pointer(PointerBuilder(/* id */ 0, AMOTION_EVENT_TOOL_TYPE_FINGER)
-                                                 .x(untransformedPoint.x)
-                                                 .y(untransformedPoint.y))
-                                .build();
-    event.transform(matrix);
-
-    injectMotionEvent(mDispatcher, event, INJECT_EVENT_TIMEOUT,
-                      InputEventInjectionSync::WAIT_FOR_RESULT);
-
-    firstWindow->consumeMotionDown();
-    secondWindow->assertNoEvents();
-}
-
 TEST_F(InputDispatcherDisplayProjectionTest, WindowGetsEventsInCorrectCoordinateSpace) {
     auto [firstWindow, secondWindow] = setupScaledDisplayScenario();
 
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index 3860901..14761ea 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -1222,6 +1222,8 @@
 void SurfaceFlinger::setActiveModeInHwcIfNeeded() {
     ATRACE_CALL();
 
+    std::optional<PhysicalDisplayId> displayToUpdateImmediately;
+
     for (const auto& iter : mDisplays) {
         const auto& display = iter.second;
         if (!display || !display->isInternal()) {
@@ -1286,8 +1288,26 @@
         }
         mScheduler->onNewVsyncPeriodChangeTimeline(outTimeline);
 
-        // Scheduler will submit an empty frame to HWC if needed.
-        mSetActiveModePending = true;
+        if (outTimeline.refreshRequired) {
+            // Scheduler will submit an empty frame to HWC.
+            mSetActiveModePending = true;
+        } else {
+            // Updating the internal state should be done outside the loop,
+            // because it can recreate a DisplayDevice and modify mDisplays
+            // which will invalidate the iterator.
+            displayToUpdateImmediately = display->getPhysicalId();
+        }
+    }
+
+    if (displayToUpdateImmediately) {
+        updateInternalStateWithChangedMode();
+
+        const auto display = getDisplayDeviceLocked(*displayToUpdateImmediately);
+        const auto desiredActiveMode = display->getDesiredActiveMode();
+        if (desiredActiveMode &&
+            display->getActiveMode()->getId() == desiredActiveMode->mode->getId()) {
+            desiredActiveModeChangeDone(display);
+        }
     }
 }
 
diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h
index eb934b6..7339b3b 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -660,8 +660,7 @@
     // Sets the desired active mode bit. It obtains the lock, and sets mDesiredActiveMode.
     void setDesiredActiveMode(const ActiveModeInfo& info) REQUIRES(mStateLock);
     status_t setActiveModeFromBackdoor(const sp<IBinder>& displayToken, int id);
-    // Once HWC has returned the present fence, this sets the active mode and a new refresh
-    // rate in SF.
+    // Sets the active mode and a new refresh rate in SF.
     void updateInternalStateWithChangedMode() REQUIRES(mStateLock);
     // Calls to setActiveMode on the main thread if there is a pending mode change
     // that needs to be applied.
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index 5c9e1c5..5f47a1a 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -71,6 +71,7 @@
         "MessageQueueTest.cpp",
         "SurfaceFlinger_CreateDisplayTest.cpp",
         "SurfaceFlinger_DestroyDisplayTest.cpp",
+        "SurfaceFlinger_DisplayModeSwitching.cpp",
         "SurfaceFlinger_DisplayTransactionCommitTest.cpp",
         "SurfaceFlinger_GetDisplayNativePrimariesTest.cpp",
         "SurfaceFlinger_HotplugTest.cpp",
diff --git a/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp b/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
new file mode 100644
index 0000000..9796a70
--- /dev/null
+++ b/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
@@ -0,0 +1,295 @@
+/*
+ * 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.
+ */
+
+#include "mock/MockEventThread.h"
+#undef LOG_TAG
+#define LOG_TAG "LibSurfaceFlingerUnittests"
+
+#include "DisplayTransactionTestHelpers.h"
+
+#include <scheduler/Fps.h>
+
+namespace android {
+namespace {
+
+using android::hardware::graphics::composer::V2_4::Error;
+using android::hardware::graphics::composer::V2_4::VsyncPeriodChangeTimeline;
+
+class DisplayModeSwitchingTest : public DisplayTransactionTest {
+public:
+    void SetUp() override {
+        injectFakeBufferQueueFactory();
+        injectFakeNativeWindowSurfaceFactory();
+
+        PrimaryDisplayVariant::setupHwcHotplugCallExpectations(this);
+        PrimaryDisplayVariant::setupFramebufferConsumerBufferQueueCallExpectations(this);
+        PrimaryDisplayVariant::setupFramebufferProducerBufferQueueCallExpectations(this);
+        PrimaryDisplayVariant::setupNativeWindowSurfaceCreationCallExpectations(this);
+        PrimaryDisplayVariant::setupHwcGetActiveConfigCallExpectations(this);
+
+        mFlinger.onComposerHalHotplug(PrimaryDisplayVariant::HWC_DISPLAY_ID, Connection::CONNECTED);
+
+        mDisplay = PrimaryDisplayVariant::makeFakeExistingDisplayInjector(this)
+                           .setSupportedModes({kDisplayMode60, kDisplayMode90, kDisplayMode120,
+                                               kDisplayMode90DifferentResolution})
+                           .setActiveMode(kDisplayModeId60)
+                           .inject();
+
+        setupScheduler();
+
+        // isVsyncPeriodSwitchSupported should return true, otherwise the SF's HWC proxy
+        // will call setActiveConfig instead of setActiveConfigWithConstraints.
+        ON_CALL(*mComposer, isVsyncPeriodSwitchSupported()).WillByDefault(Return(true));
+    }
+
+protected:
+    void setupScheduler();
+    void testChangeRefreshRate(bool isDisplayActive, bool isRefreshRequired);
+
+    sp<DisplayDevice> mDisplay;
+    mock::EventThread* mAppEventThread;
+
+    const DisplayModeId kDisplayModeId60 = DisplayModeId(0);
+    const DisplayModePtr kDisplayMode60 =
+            DisplayMode::Builder(hal::HWConfigId(kDisplayModeId60.value()))
+                    .setId(kDisplayModeId60)
+                    .setPhysicalDisplayId(PrimaryDisplayVariant::DISPLAY_ID::get())
+                    .setVsyncPeriod((60_Hz).getPeriodNsecs())
+                    .setGroup(0)
+                    .setHeight(1000)
+                    .setWidth(1000)
+                    .build();
+
+    const DisplayModeId kDisplayModeId90 = DisplayModeId(1);
+    const DisplayModePtr kDisplayMode90 =
+            DisplayMode::Builder(hal::HWConfigId(kDisplayModeId90.value()))
+                    .setId(kDisplayModeId90)
+                    .setPhysicalDisplayId(PrimaryDisplayVariant::DISPLAY_ID::get())
+                    .setVsyncPeriod((90_Hz).getPeriodNsecs())
+                    .setGroup(1)
+                    .setHeight(1000)
+                    .setWidth(1000)
+                    .build();
+
+    const DisplayModeId kDisplayModeId120 = DisplayModeId(2);
+    const DisplayModePtr kDisplayMode120 =
+            DisplayMode::Builder(hal::HWConfigId(kDisplayModeId120.value()))
+                    .setId(kDisplayModeId120)
+                    .setPhysicalDisplayId(PrimaryDisplayVariant::DISPLAY_ID::get())
+                    .setVsyncPeriod((120_Hz).getPeriodNsecs())
+                    .setGroup(2)
+                    .setHeight(1000)
+                    .setWidth(1000)
+                    .build();
+
+    const DisplayModeId kDisplayModeId90DifferentResolution = DisplayModeId(3);
+    const DisplayModePtr kDisplayMode90DifferentResolution =
+            DisplayMode::Builder(hal::HWConfigId(kDisplayModeId90DifferentResolution.value()))
+                    .setId(kDisplayModeId90DifferentResolution)
+                    .setPhysicalDisplayId(PrimaryDisplayVariant::DISPLAY_ID::get())
+                    .setVsyncPeriod((90_Hz).getPeriodNsecs())
+                    .setGroup(3)
+                    .setHeight(2000)
+                    .setWidth(2000)
+                    .build();
+};
+
+void DisplayModeSwitchingTest::setupScheduler() {
+    auto eventThread = std::make_unique<mock::EventThread>();
+    mAppEventThread = eventThread.get();
+    auto sfEventThread = std::make_unique<mock::EventThread>();
+
+    EXPECT_CALL(*eventThread, registerDisplayEventConnection(_));
+    EXPECT_CALL(*eventThread, createEventConnection(_, _))
+            .WillOnce(Return(new EventThreadConnection(eventThread.get(), /*callingUid=*/0,
+                                                       ResyncCallback())));
+
+    EXPECT_CALL(*sfEventThread, registerDisplayEventConnection(_));
+    EXPECT_CALL(*sfEventThread, createEventConnection(_, _))
+            .WillOnce(Return(new EventThreadConnection(sfEventThread.get(), /*callingUid=*/0,
+                                                       ResyncCallback())));
+
+    auto vsyncController = std::make_unique<mock::VsyncController>();
+    auto vsyncTracker = std::make_unique<mock::VSyncTracker>();
+
+    EXPECT_CALL(*vsyncTracker, nextAnticipatedVSyncTimeFrom(_)).WillRepeatedly(Return(0));
+    EXPECT_CALL(*vsyncTracker, currentPeriod())
+            .WillRepeatedly(
+                    Return(TestableSurfaceFlinger::FakeHwcDisplayInjector::DEFAULT_VSYNC_PERIOD));
+    EXPECT_CALL(*vsyncTracker, nextAnticipatedVSyncTimeFrom(_)).WillRepeatedly(Return(0));
+    mFlinger.setupScheduler(std::move(vsyncController), std::move(vsyncTracker),
+                            std::move(eventThread), std::move(sfEventThread), /*callback*/ nullptr,
+                            /*hasMultipleModes*/ true);
+}
+
+TEST_F(DisplayModeSwitchingTest, changeRefreshRate_OnActiveDisplay_WithRefreshRequired) {
+    ASSERT_FALSE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId60);
+
+    mFlinger.onActiveDisplayChanged(mDisplay);
+
+    mFlinger.setDesiredDisplayModeSpecs(mDisplay->getDisplayToken().promote(),
+                                        kDisplayModeId90.value(), false, 0.f, 120.f, 0.f, 120.f);
+
+    ASSERT_TRUE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getDesiredActiveMode()->mode->getId(), kDisplayModeId90);
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId60);
+
+    // Verify that next commit will call setActiveConfigWithConstraints in HWC
+    const VsyncPeriodChangeTimeline timeline{.refreshRequired = true};
+    EXPECT_CALL(*mComposer,
+                setActiveConfigWithConstraints(PrimaryDisplayVariant::HWC_DISPLAY_ID,
+                                               hal::HWConfigId(kDisplayModeId90.value()), _, _))
+            .WillOnce(DoAll(SetArgPointee<3>(timeline), Return(Error::NONE)));
+
+    mFlinger.commit();
+
+    Mock::VerifyAndClearExpectations(mComposer);
+    ASSERT_TRUE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId60);
+
+    // Verify that the next commit will complete the mode change and send
+    // a onModeChanged event to the framework.
+
+    EXPECT_CALL(*mAppEventThread, onModeChanged(kDisplayMode90));
+    mFlinger.commit();
+    Mock::VerifyAndClearExpectations(mAppEventThread);
+
+    ASSERT_FALSE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId90);
+}
+
+TEST_F(DisplayModeSwitchingTest, changeRefreshRate_OnActiveDisplay_WithoutRefreshRequired) {
+    ASSERT_FALSE(mDisplay->getDesiredActiveMode().has_value());
+
+    mFlinger.onActiveDisplayChanged(mDisplay);
+
+    mFlinger.setDesiredDisplayModeSpecs(mDisplay->getDisplayToken().promote(),
+                                        kDisplayModeId90.value(), true, 0.f, 120.f, 0.f, 120.f);
+
+    ASSERT_TRUE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getDesiredActiveMode()->mode->getId(), kDisplayModeId90);
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId60);
+
+    // Verify that next commit will call setActiveConfigWithConstraints in HWC
+    // and complete the mode change.
+    const VsyncPeriodChangeTimeline timeline{.refreshRequired = false};
+    EXPECT_CALL(*mComposer,
+                setActiveConfigWithConstraints(PrimaryDisplayVariant::HWC_DISPLAY_ID,
+                                               hal::HWConfigId(kDisplayModeId90.value()), _, _))
+            .WillOnce(DoAll(SetArgPointee<3>(timeline), Return(Error::NONE)));
+
+    EXPECT_CALL(*mAppEventThread, onModeChanged(kDisplayMode90));
+
+    mFlinger.commit();
+
+    ASSERT_FALSE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId90);
+}
+
+TEST_F(DisplayModeSwitchingTest, twoConsecutiveSetDesiredDisplayModeSpecs) {
+    // Test that if we call setDesiredDisplayModeSpecs while a previous mode change
+    // is still being processed the later call will be respected.
+
+    ASSERT_FALSE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId60);
+
+    mFlinger.onActiveDisplayChanged(mDisplay);
+
+    mFlinger.setDesiredDisplayModeSpecs(mDisplay->getDisplayToken().promote(),
+                                        kDisplayModeId90.value(), false, 0.f, 120.f, 0.f, 120.f);
+
+    const VsyncPeriodChangeTimeline timeline{.refreshRequired = true};
+    EXPECT_CALL(*mComposer,
+                setActiveConfigWithConstraints(PrimaryDisplayVariant::HWC_DISPLAY_ID,
+                                               hal::HWConfigId(kDisplayModeId90.value()), _, _))
+            .WillOnce(DoAll(SetArgPointee<3>(timeline), Return(Error::NONE)));
+
+    mFlinger.commit();
+
+    mFlinger.setDesiredDisplayModeSpecs(mDisplay->getDisplayToken().promote(),
+                                        kDisplayModeId120.value(), false, 0.f, 180.f, 0.f, 180.f);
+
+    ASSERT_TRUE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getDesiredActiveMode()->mode->getId(), kDisplayModeId120);
+
+    EXPECT_CALL(*mComposer,
+                setActiveConfigWithConstraints(PrimaryDisplayVariant::HWC_DISPLAY_ID,
+                                               hal::HWConfigId(kDisplayModeId120.value()), _, _))
+            .WillOnce(DoAll(SetArgPointee<3>(timeline), Return(Error::NONE)));
+
+    mFlinger.commit();
+
+    ASSERT_TRUE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getDesiredActiveMode()->mode->getId(), kDisplayModeId120);
+
+    mFlinger.commit();
+
+    ASSERT_FALSE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId120);
+}
+
+TEST_F(DisplayModeSwitchingTest, changeResolution_OnActiveDisplay_WithoutRefreshRequired) {
+    ASSERT_FALSE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId60);
+
+    mFlinger.onActiveDisplayChanged(mDisplay);
+
+    mFlinger.setDesiredDisplayModeSpecs(mDisplay->getDisplayToken().promote(),
+                                        kDisplayModeId90DifferentResolution.value(), false, 0.f,
+                                        120.f, 0.f, 120.f);
+
+    ASSERT_TRUE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getDesiredActiveMode()->mode->getId(), kDisplayModeId90DifferentResolution);
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId60);
+
+    // Verify that next commit will call setActiveConfigWithConstraints in HWC
+    // and complete the mode change.
+    const VsyncPeriodChangeTimeline timeline{.refreshRequired = false};
+    EXPECT_CALL(*mComposer,
+                setActiveConfigWithConstraints(PrimaryDisplayVariant::HWC_DISPLAY_ID,
+                                               hal::HWConfigId(
+                                                       kDisplayModeId90DifferentResolution.value()),
+                                               _, _))
+            .WillOnce(DoAll(SetArgPointee<3>(timeline), Return(Error::NONE)));
+
+    EXPECT_CALL(*mAppEventThread, onHotplugReceived(mDisplay->getPhysicalId(), true));
+
+    // Misc expecations. We don't need to enforce these method calls, but since the helper methods
+    // already set expectations we should add new ones here, otherwise the test will fail.
+    EXPECT_CALL(*mConsumer, setDefaultBufferSize(2000, 2000)).WillOnce(Return(NO_ERROR));
+    EXPECT_CALL(*mConsumer, consumerConnect(_, false)).WillOnce(Return(NO_ERROR));
+    EXPECT_CALL(*mComposer, setClientTargetSlotCount(_)).WillOnce(Return(hal::Error::NONE));
+
+    // Create a new native surface to be used by the recreated display.
+    mNativeWindowSurface = nullptr;
+    injectFakeNativeWindowSurfaceFactory();
+    PrimaryDisplayVariant::setupNativeWindowSurfaceCreationCallExpectations(this);
+
+    const auto displayToken = mDisplay->getDisplayToken().promote();
+
+    mFlinger.commit();
+
+    // The DisplayDevice will be destroyed and recreated,
+    // so we need to update with the new instance.
+    mDisplay = mFlinger.getDisplay(displayToken);
+
+    ASSERT_FALSE(mDisplay->getDesiredActiveMode().has_value());
+    ASSERT_EQ(mDisplay->getActiveMode()->getId(), kDisplayModeId90DifferentResolution);
+}
+
+} // namespace
+} // namespace android
diff --git a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
index 4473e01..7f949b9 100644
--- a/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
+++ b/services/surfaceflinger/tests/unittests/TestableSurfaceFlinger.h
@@ -24,6 +24,7 @@
 #include <compositionengine/impl/OutputLayerCompositionState.h>
 #include <compositionengine/mock/DisplaySurface.h>
 #include <gui/ScreenCaptureResults.h>
+#include <algorithm>
 
 #include "BufferQueueLayer.h"
 #include "BufferStateLayer.h"
@@ -307,6 +308,11 @@
         return mFlinger->destroyDisplay(displayToken);
     }
 
+    auto getDisplay(const sp<IBinder>& displayToken) {
+        Mutex::Autolock lock(mFlinger->mStateLock);
+        return mFlinger->getDisplayDeviceLocked(displayToken);
+    }
+
     void enableHalVirtualDisplays(bool enable) { mFlinger->enableHalVirtualDisplays(enable); }
 
     auto setupNewDisplayDeviceInternal(
@@ -395,6 +401,21 @@
         return SurfaceFlinger::calculateMaxAcquiredBufferCount(refreshRate, presentLatency);
     }
 
+    auto setDesiredDisplayModeSpecs(const sp<IBinder>& displayToken, ui::DisplayModeId defaultMode,
+                                    bool allowGroupSwitching, float primaryRefreshRateMin,
+                                    float primaryRefreshRateMax, float appRequestRefreshRateMin,
+                                    float appRequestRefreshRateMax) {
+        return mFlinger->setDesiredDisplayModeSpecs(displayToken, defaultMode, allowGroupSwitching,
+                                                    primaryRefreshRateMin, primaryRefreshRateMax,
+                                                    appRequestRefreshRateMin,
+                                                    appRequestRefreshRateMax);
+    }
+
+    void onActiveDisplayChanged(const sp<DisplayDevice>& activeDisplay) {
+        Mutex::Autolock lock(mFlinger->mStateLock);
+        mFlinger->onActiveDisplayChangedLocked(activeDisplay);
+    }
+
     /* ------------------------------------------------------------------------
      * Read-only access to private data to assert post-conditions.
      */
@@ -482,7 +503,7 @@
         static constexpr hal::HWDisplayId DEFAULT_HWC_DISPLAY_ID = 1000;
         static constexpr int32_t DEFAULT_WIDTH = 1920;
         static constexpr int32_t DEFAULT_HEIGHT = 1280;
-        static constexpr int32_t DEFAULT_VSYNC_PERIOD = 16'666'666;
+        static constexpr int32_t DEFAULT_VSYNC_PERIOD = 16'666'667;
         static constexpr int32_t DEFAULT_CONFIG_GROUP = 7;
         static constexpr int32_t DEFAULT_DPI = 320;
         static constexpr hal::HWConfigId DEFAULT_ACTIVE_CONFIG = 0;
@@ -636,10 +657,10 @@
             mCreationArgs.connectionType = connectionType;
             mCreationArgs.isPrimary = isPrimary;
 
-            mActiveModeId = DisplayModeId(0);
+            mCreationArgs.activeModeId = DisplayModeId(0);
             DisplayModePtr activeMode =
                     DisplayMode::Builder(FakeHwcDisplayInjector::DEFAULT_ACTIVE_CONFIG)
-                            .setId(mActiveModeId)
+                            .setId(mCreationArgs.activeModeId)
                             .setPhysicalDisplayId(PhysicalDisplayId::fromPort(0))
                             .setWidth(FakeHwcDisplayInjector::DEFAULT_WIDTH)
                             .setHeight(FakeHwcDisplayInjector::DEFAULT_HEIGHT)
@@ -675,7 +696,7 @@
         auto& mutableDisplayDevice() { return mFlinger.mutableDisplays()[mDisplayToken]; }
 
         auto& setActiveMode(DisplayModeId mode) {
-            mActiveModeId = mode;
+            mCreationArgs.activeModeId = mode;
             return *this;
         }
 
@@ -730,14 +751,29 @@
                 const auto physicalId = PhysicalDisplayId::tryCast(*displayId);
                 LOG_ALWAYS_FATAL_IF(!physicalId);
                 LOG_ALWAYS_FATAL_IF(!mHwcDisplayId);
-                state.physical = {.id = *physicalId, .type = *type, .hwcDisplayId = *mHwcDisplayId};
+
+                const DisplayModePtr activeModePtr =
+                        *std::find_if(mCreationArgs.supportedModes.begin(),
+                                      mCreationArgs.supportedModes.end(), [&](DisplayModePtr mode) {
+                                          return mode->getId() == mCreationArgs.activeModeId;
+                                      });
+                state.physical = {.id = *physicalId,
+                                  .type = *type,
+                                  .hwcDisplayId = *mHwcDisplayId,
+                                  .deviceProductInfo = {},
+                                  .supportedModes = mCreationArgs.supportedModes,
+                                  .activeMode = activeModePtr};
             }
 
             state.isSecure = mCreationArgs.isSecure;
 
+            mCreationArgs.refreshRateConfigs =
+                    std::make_shared<scheduler::RefreshRateConfigs>(mCreationArgs.supportedModes,
+                                                                    mCreationArgs.activeModeId);
+
             sp<DisplayDevice> device = new DisplayDevice(mCreationArgs);
             if (!device->isVirtual()) {
-                device->setActiveMode(mActiveModeId);
+                device->setActiveMode(mCreationArgs.activeModeId);
             }
             mFlinger.mutableDisplays().emplace(mDisplayToken, device);
             mFlinger.mutableCurrentState().displays.add(mDisplayToken, state);
@@ -755,7 +791,6 @@
         sp<BBinder> mDisplayToken = new BBinder();
         DisplayDeviceCreationArgs mCreationArgs;
         const std::optional<hal::HWDisplayId> mHwcDisplayId;
-        DisplayModeId mActiveModeId;
     };
 
 private: