Merge changes from topic "preferredDisplayModeId60120" into sc-dev

* changes:
  Enable preferredDisplayModeId for frame rate override
  Add preferredMaxDisplayRefreshRate
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 2d58520..dce3fef 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -190,6 +190,8 @@
      * has a preference.
      * @param requestedModeId The preferred mode id for the top-most visible window that has a
      * preference.
+     * @param requestedMaxRefreshRate The preferred highest refresh rate for the top-most visible
+     *                                window that has a preference.
      * @param requestedMinimalPostProcessing The preferred minimal post processing setting for the
      * display. This is true when there is at least one visible window that wants minimal post
      * processng on.
@@ -197,8 +199,8 @@
      * prior to call to performTraversalInTransactionFromWindowManager.
      */
     public abstract void setDisplayProperties(int displayId, boolean hasContent,
-            float requestedRefreshRate, int requestedModeId, boolean requestedMinimalPostProcessing,
-            boolean inTraversal);
+            float requestedRefreshRate, int requestedModeId, float requestedMaxRefreshRate,
+            boolean requestedMinimalPostProcessing, boolean inTraversal);
 
     /**
      * Applies an offset to the contents of a display, for example to avoid burn-in.
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index c32ab3a..c1e394d 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3010,6 +3010,14 @@
         public int preferredDisplayModeId;
 
         /**
+         * The max display refresh rate while the window is in focus.
+         *
+         * This value is ignored if {@link #preferredDisplayModeId} is set.
+         * @hide
+         */
+        public float preferredMaxDisplayRefreshRate;
+
+        /**
          * An internal annotation for flags that can be specified to {@link #systemUiVisibility}
          * and {@link #subtreeSystemUiVisibility}.
          *
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
index 388d72d..ae018ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationShadeWindowControllerImpl.java
@@ -61,7 +61,6 @@
 import java.lang.ref.WeakReference;
 import java.lang.reflect.Field;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -85,7 +84,7 @@
     private final LayoutParams mLpChanged;
     private final boolean mKeyguardScreenRotation;
     private final long mLockScreenDisplayTimeout;
-    private final Display.Mode mKeyguardDisplayMode;
+    private final float mKeyguardRefreshRate;
     private final KeyguardViewMediator mKeyguardViewMediator;
     private final KeyguardBypassController mKeyguardBypassController;
     private ViewGroup mNotificationShadeView;
@@ -135,14 +134,8 @@
         // Running on the highest frame rate available can be expensive.
         // Let's specify a preferred refresh rate, and allow higher FPS only when we
         // know that we're not falsing (because we unlocked.)
-        int keyguardRefreshRate = context.getResources()
+        mKeyguardRefreshRate = context.getResources()
                 .getInteger(R.integer.config_keyguardRefreshRate);
-        // Find supported display mode with the same resolution and requested refresh rate.
-        mKeyguardDisplayMode = Arrays.stream(supportedModes).filter(mode ->
-                (int) mode.getRefreshRate() == keyguardRefreshRate
-                        && mode.getPhysicalWidth() == currentMode.getPhysicalWidth()
-                        && mode.getPhysicalHeight() == currentMode.getPhysicalHeight())
-                .findFirst().orElse(null);
     }
 
     /**
@@ -273,16 +266,17 @@
             mLpChanged.privateFlags &= ~LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
         }
 
-        if (mKeyguardDisplayMode != null) {
+        if (mKeyguardRefreshRate > 0) {
             boolean bypassOnKeyguard = mKeyguardBypassController.getBypassEnabled()
                     && state.mStatusBarState == StatusBarState.KEYGUARD
                     && !state.mKeyguardFadingAway && !state.mKeyguardGoingAway;
             if (state.mDozing || bypassOnKeyguard) {
-                mLpChanged.preferredDisplayModeId = mKeyguardDisplayMode.getModeId();
+                mLpChanged.preferredMaxDisplayRefreshRate = mKeyguardRefreshRate;
             } else {
-                mLpChanged.preferredDisplayModeId = 0;
+                mLpChanged.preferredMaxDisplayRefreshRate = 0;
             }
-            Trace.setCounter("display_mode_id", mLpChanged.preferredDisplayModeId);
+            Trace.setCounter("display_max_refresh_rate",
+                    (long) mLpChanged.preferredMaxDisplayRefreshRate);
         }
     }
 
@@ -669,7 +663,7 @@
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
         pw.println(TAG + ":");
-        pw.println("  mKeyguardDisplayMode=" + mKeyguardDisplayMode);
+        pw.println("  mKeyguardRefreshRate=" + mKeyguardRefreshRate);
         pw.println(mCurrentState);
         if (mNotificationShadeView != null && mNotificationShadeView.getViewRootImpl() != null) {
             mNotificationShadeView.getViewRootImpl().dump("  ", pw);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index f75f3e1..d4920f5 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1498,8 +1498,8 @@
     }
 
     private void setDisplayPropertiesInternal(int displayId, boolean hasContent,
-            float requestedRefreshRate, int requestedModeId, boolean preferMinimalPostProcessing,
-            boolean inTraversal) {
+            float requestedRefreshRate, int requestedModeId, float requestedMaxRefreshRate,
+            boolean preferMinimalPostProcessing, boolean inTraversal) {
         synchronized (mSyncRoot) {
             final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId);
             if (display == null) {
@@ -1523,8 +1523,8 @@
                 requestedModeId = display.getDisplayInfoLocked().findDefaultModeByRefreshRate(
                         requestedRefreshRate).getModeId();
             }
-            mDisplayModeDirector.getAppRequestObserver().setAppRequestedMode(
-                    displayId, requestedModeId);
+            mDisplayModeDirector.getAppRequestObserver().setAppRequest(
+                    displayId, requestedModeId, requestedMaxRefreshRate);
 
             if (display.getDisplayInfoLocked().minimalPostProcessingSupported) {
                 boolean mppRequest = mMinimalPostProcessingAllowed && preferMinimalPostProcessing;
@@ -3189,10 +3189,11 @@
 
         @Override
         public void setDisplayProperties(int displayId, boolean hasContent,
-                float requestedRefreshRate, int requestedMode,
+                float requestedRefreshRate, int requestedMode, float requestedMaxRefreshRate,
                 boolean requestedMinimalPostProcessing, boolean inTraversal) {
             setDisplayPropertiesInternal(displayId, hasContent, requestedRefreshRate,
-                    requestedMode, requestedMinimalPostProcessing, inTraversal);
+                    requestedMode, requestedMaxRefreshRate, requestedMinimalPostProcessing,
+                    inTraversal);
         }
 
         @Override
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index 67779a2..997f0e5 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -198,6 +198,8 @@
         public float maxRefreshRate;
         public int width;
         public int height;
+        public boolean disableRefreshRateSwitching;
+        public float baseModeRefreshRate;
 
         VoteSummary() {
             reset();
@@ -208,6 +210,8 @@
             maxRefreshRate = Float.POSITIVE_INFINITY;
             width = Vote.INVALID_SIZE;
             height = Vote.INVALID_SIZE;
+            disableRefreshRateSwitching = false;
+            baseModeRefreshRate = 0f;
         }
     }
 
@@ -229,13 +233,20 @@
             // For refresh rates, just use the tightest bounds of all the votes
             summary.minRefreshRate = Math.max(summary.minRefreshRate, vote.refreshRateRange.min);
             summary.maxRefreshRate = Math.min(summary.maxRefreshRate, vote.refreshRateRange.max);
-            // For display size, use only the first vote we come across (i.e. the highest
-            // priority vote that includes the width / height).
+            // For display size, disable refresh rate switching and base mode refresh rate use only
+            // the first vote we come across (i.e. the highest priority vote that includes the
+            // attribute).
             if (summary.height == Vote.INVALID_SIZE && summary.width == Vote.INVALID_SIZE
                     && vote.height > 0 && vote.width > 0) {
                 summary.width = vote.width;
                 summary.height = vote.height;
             }
+            if (!summary.disableRefreshRateSwitching && vote.disableRefreshRateSwitching) {
+                summary.disableRefreshRateSwitching = true;
+            }
+            if (summary.baseModeRefreshRate == 0f && vote.baseModeRefreshRate > 0f) {
+                summary.baseModeRefreshRate = vote.baseModeRefreshRate;
+            }
         }
     }
 
@@ -260,13 +271,14 @@
                 return new DesiredDisplayModeSpecs();
             }
 
-            int[] availableModes = new int[]{defaultMode.getModeId()};
+            ArrayList<Display.Mode> availableModes = new ArrayList<>();
+            availableModes.add(defaultMode);
             VoteSummary primarySummary = new VoteSummary();
             int lowestConsideredPriority = Vote.MIN_PRIORITY;
             int highestConsideredPriority = Vote.MAX_PRIORITY;
 
             if (mAlwaysRespectAppRequest) {
-                lowestConsideredPriority = Vote.PRIORITY_APP_REQUEST_REFRESH_RATE;
+                lowestConsideredPriority = Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE;
                 highestConsideredPriority = Vote.PRIORITY_APP_REQUEST_SIZE;
             }
 
@@ -286,16 +298,19 @@
                 }
 
                 availableModes = filterModes(modes, primarySummary);
-                if (availableModes.length > 0) {
+                if (!availableModes.isEmpty()) {
                     if (mLoggingEnabled) {
-                        Slog.w(TAG, "Found available modes=" + Arrays.toString(availableModes)
+                        Slog.w(TAG, "Found available modes=" + availableModes
                                 + " with lowest priority considered "
                                 + Vote.priorityToString(lowestConsideredPriority)
                                 + " and constraints: "
                                 + "width=" + primarySummary.width
                                 + ", height=" + primarySummary.height
                                 + ", minRefreshRate=" + primarySummary.minRefreshRate
-                                + ", maxRefreshRate=" + primarySummary.maxRefreshRate);
+                                + ", maxRefreshRate=" + primarySummary.maxRefreshRate
+                                + ", disableRefreshRateSwitching="
+                                + primarySummary.disableRefreshRateSwitching
+                                + ", baseModeRefreshRate=" + primarySummary.baseModeRefreshRate);
                     }
                     break;
                 }
@@ -307,7 +322,10 @@
                             + "width=" + primarySummary.width
                             + ", height=" + primarySummary.height
                             + ", minRefreshRate=" + primarySummary.minRefreshRate
-                            + ", maxRefreshRate=" + primarySummary.maxRefreshRate);
+                            + ", maxRefreshRate=" + primarySummary.maxRefreshRate
+                            + ", disableRefreshRateSwitching="
+                            + primarySummary.disableRefreshRateSwitching
+                            + ", baseModeRefreshRate=" + primarySummary.baseModeRefreshRate);
                 }
 
                 // If we haven't found anything with the current set of votes, drop the
@@ -332,26 +350,38 @@
                                 appRequestSummary.maxRefreshRate));
             }
 
-            int baseModeId = INVALID_DISPLAY_MODE_ID;
+            // Select the base mode id based on the base mode refresh rate, if available, since this
+            // will be the mode id the app voted for.
+            Display.Mode baseMode = null;
+            for (Display.Mode availableMode : availableModes) {
+                if (primarySummary.baseModeRefreshRate
+                        >= availableMode.getRefreshRate() - FLOAT_TOLERANCE
+                        && primarySummary.baseModeRefreshRate
+                        <= availableMode.getRefreshRate() + FLOAT_TOLERANCE) {
+                    baseMode = availableMode;
+                }
+            }
 
             // Select the default mode if available. This is important because SurfaceFlinger
             // can do only seamless switches by default. Some devices (e.g. TV) don't support
             // seamless switching so the mode we select here won't be changed.
-            for (int availableMode : availableModes) {
-                if (availableMode == defaultMode.getModeId()) {
-                    baseModeId = defaultMode.getModeId();
-                    break;
+            if (baseMode == null) {
+                for (Display.Mode availableMode : availableModes) {
+                    if (availableMode.getModeId() == defaultMode.getModeId()) {
+                        baseMode = defaultMode;
+                        break;
+                    }
                 }
             }
 
             // If the application requests a display mode by setting
             // LayoutParams.preferredDisplayModeId, it will be the only available mode and it'll
             // be stored as baseModeId.
-            if (baseModeId == INVALID_DISPLAY_MODE_ID && availableModes.length > 0) {
-                baseModeId = availableModes[0];
+            if (baseMode == null && !availableModes.isEmpty()) {
+                baseMode = availableModes.get(0);
             }
 
-            if (baseModeId == INVALID_DISPLAY_MODE_ID) {
+            if (baseMode == null) {
                 Slog.w(TAG, "Can't find a set of allowed modes which satisfies the votes. Falling"
                         + " back to the default mode. Display = " + displayId + ", votes = " + votes
                         + ", supported modes = " + Arrays.toString(modes));
@@ -363,31 +393,19 @@
                         new RefreshRateRange(fps, fps));
             }
 
-            if (mModeSwitchingType == DisplayManager.SWITCHING_TYPE_NONE) {
-                Display.Mode baseMode = null;
-                for (Display.Mode mode : modes) {
-                    if (mode.getModeId() == baseModeId) {
-                        baseMode = mode;
-                        break;
-                    }
-                }
-                if (baseMode == null) {
-                    // This should never happen.
-                    throw new IllegalStateException(
-                            "The base mode with id " + baseModeId
-                                    + " is not in the list of supported modes.");
-                }
+            if (mModeSwitchingType == DisplayManager.SWITCHING_TYPE_NONE
+                    || primarySummary.disableRefreshRateSwitching) {
                 float fps = baseMode.getRefreshRate();
-                return new DesiredDisplayModeSpecs(baseModeId,
-                        /*allowGroupSwitching */ false,
-                        new RefreshRateRange(fps, fps),
-                        new RefreshRateRange(fps, fps));
+                primarySummary.minRefreshRate = primarySummary.maxRefreshRate = fps;
+                if (mModeSwitchingType == DisplayManager.SWITCHING_TYPE_NONE) {
+                    appRequestSummary.minRefreshRate = appRequestSummary.maxRefreshRate = fps;
+                }
             }
 
             boolean allowGroupSwitching =
                     mModeSwitchingType == DisplayManager.SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS;
 
-            return new DesiredDisplayModeSpecs(baseModeId,
+            return new DesiredDisplayModeSpecs(baseMode.getModeId(),
                     allowGroupSwitching,
                     new RefreshRateRange(
                             primarySummary.minRefreshRate, primarySummary.maxRefreshRate),
@@ -396,8 +414,10 @@
         }
     }
 
-    private int[] filterModes(Display.Mode[] supportedModes, VoteSummary summary) {
+    private ArrayList<Display.Mode> filterModes(Display.Mode[] supportedModes,
+            VoteSummary summary) {
         ArrayList<Display.Mode> availableModes = new ArrayList<>();
+        boolean missingBaseModeRefreshRate = summary.baseModeRefreshRate > 0f;
         for (Display.Mode mode : supportedModes) {
             if (mode.getPhysicalWidth() != summary.width
                     || mode.getPhysicalHeight() != summary.height) {
@@ -426,13 +446,16 @@
                 continue;
             }
             availableModes.add(mode);
+            if (mode.getRefreshRate() >= summary.baseModeRefreshRate - FLOAT_TOLERANCE
+                    && mode.getRefreshRate() <= summary.baseModeRefreshRate + FLOAT_TOLERANCE) {
+                missingBaseModeRefreshRate = false;
+            }
         }
-        final int size = availableModes.size();
-        int[] availableModeIds = new int[size];
-        for (int i = 0; i < size; i++) {
-            availableModeIds[i] = availableModes.get(i).getModeId();
+        if (missingBaseModeRefreshRate) {
+            return new ArrayList<>();
         }
-        return availableModeIds;
+
+        return availableModes;
     }
 
     /**
@@ -912,37 +935,52 @@
         // by all other considerations. It acts to set a default frame rate for a device.
         public static final int PRIORITY_DEFAULT_REFRESH_RATE = 0;
 
-        // FLICKER votes for a single refresh rate like [60,60], [90,90] or null.
-        // If the higher voters result is a range, it will fix the rate to a single choice.
-        // It's used to avoid refresh rate switches in certain conditions which may result in the
-        // user seeing the display flickering when the switches occur.
-        public static final int PRIORITY_FLICKER = 1;
+        // PRIORITY_FLICKER_REFRESH_RATE votes for a single refresh rate like [60,60], [90,90] or
+        // null. It is used to set a preferred refresh rate value in case the higher priority votes
+        // result is a range.
+        public static final int PRIORITY_FLICKER_REFRESH_RATE = 1;
 
         // SETTING_MIN_REFRESH_RATE is used to propose a lower bound of display refresh rate.
         // It votes [MIN_REFRESH_RATE, Float.POSITIVE_INFINITY]
         public static final int PRIORITY_USER_SETTING_MIN_REFRESH_RATE = 2;
 
+        // APP_REQUEST_MAX_REFRESH_RATE is used to for internal apps to limit the refresh
+        // rate in certain cases, mostly to preserve power.
+        // It votes to [0, APP_REQUEST_MAX_REFRESH_RATE].
+        public static final int PRIORITY_APP_REQUEST_MAX_REFRESH_RATE = 3;
+
         // We split the app request into different priorities in case we can satisfy one desire
         // without the other.
 
         // Application can specify preferred refresh rate with below attrs.
         // @see android.view.WindowManager.LayoutParams#preferredRefreshRate
         // @see android.view.WindowManager.LayoutParams#preferredDisplayModeId
-        // System also forces some apps like denylisted app to run at a lower refresh rate.
+        // These translates into votes for the base mode refresh rate and resolution to be
+        // used by SurfaceFlinger as the policy of choosing the display mode. The system also
+        // forces some apps like denylisted app to run at a lower refresh rate.
         // @see android.R.array#config_highRefreshRateBlacklist
-        public static final int PRIORITY_APP_REQUEST_REFRESH_RATE = 3;
-        public static final int PRIORITY_APP_REQUEST_SIZE = 4;
+        // The preferred refresh rate is set on the main surface of the app outside of
+        // DisplayModeDirector.
+        // @see com.android.server.wm.WindowState#updateFrameRateSelectionPriorityIfNeeded
+        public static final int PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE = 4;
+        public static final int PRIORITY_APP_REQUEST_SIZE = 5;
 
         // SETTING_PEAK_REFRESH_RATE has a high priority and will restrict the bounds of the rest
         // of low priority voters. It votes [0, max(PEAK, MIN)]
-        public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 5;
+        public static final int PRIORITY_USER_SETTING_PEAK_REFRESH_RATE = 6;
 
         // LOW_POWER_MODE force display to [0, 60HZ] if Settings.Global.LOW_POWER_MODE is on.
-        public static final int PRIORITY_LOW_POWER_MODE = 6;
+        public static final int PRIORITY_LOW_POWER_MODE = 7;
+
+        // PRIORITY_FLICKER_REFRESH_RATE_SWITCH votes for disabling refresh rate switching. If the
+        // higher priority voters' result is a range, it will fix the rate to a single choice.
+        // It's used to avoid refresh rate switches in certain conditions which may result in the
+        // user seeing the display flickering when the switches occur.
+        public static final int PRIORITY_FLICKER_REFRESH_RATE_SWITCH = 8;
 
         // The Under-Display Fingerprint Sensor (UDFPS) needs the refresh rate to be locked in order
         // to function, so this needs to be the highest priority of all votes.
-        public static final int PRIORITY_UDFPS = 7;
+        public static final int PRIORITY_UDFPS = 9;
 
         // Whenever a new priority is added, remember to update MIN_PRIORITY, MAX_PRIORITY, and
         // APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF, as well as priorityToString.
@@ -953,7 +991,7 @@
         // The cutoff for the app request refresh rate range. Votes with priorities lower than this
         // value will not be considered when constructing the app request refresh rate range.
         public static final int APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF =
-                PRIORITY_APP_REQUEST_REFRESH_RATE;
+                PRIORITY_APP_REQUEST_MAX_REFRESH_RATE;
 
         /**
          * A value signifying an invalid width or height in a vote.
@@ -973,32 +1011,64 @@
          */
         public final RefreshRateRange refreshRateRange;
 
+        /**
+         * Whether refresh rate switching should be disabled (i.e. the refresh rate range is
+         * a single value).
+         */
+        public final boolean disableRefreshRateSwitching;
+
+        /**
+         * The base mode refresh rate to be used for this display. This would be used when deciding
+         * the base mode id.
+         */
+        public final float baseModeRefreshRate;
+
         public static Vote forRefreshRates(float minRefreshRate, float maxRefreshRate) {
-            return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate);
+            return new Vote(INVALID_SIZE, INVALID_SIZE, minRefreshRate, maxRefreshRate,
+                    minRefreshRate == maxRefreshRate, 0f);
         }
 
         public static Vote forSize(int width, int height) {
-            return new Vote(width, height, 0f, Float.POSITIVE_INFINITY);
+            return new Vote(width, height, 0f, Float.POSITIVE_INFINITY, false,
+                    0f);
+        }
+
+        public static Vote forDisableRefreshRateSwitching() {
+            return new Vote(INVALID_SIZE, INVALID_SIZE, 0f, Float.POSITIVE_INFINITY, true,
+                    0f);
+        }
+
+        public static Vote forBaseModeRefreshRate(float baseModeRefreshRate) {
+            return new Vote(INVALID_SIZE, INVALID_SIZE, 0f, Float.POSITIVE_INFINITY, false,
+                    baseModeRefreshRate);
         }
 
         private Vote(int width, int height,
-                float minRefreshRate, float maxRefreshRate) {
+                float minRefreshRate, float maxRefreshRate,
+                boolean disableRefreshRateSwitching,
+                float baseModeRefreshRate) {
             this.width = width;
             this.height = height;
             this.refreshRateRange =
                     new RefreshRateRange(minRefreshRate, maxRefreshRate);
+            this.disableRefreshRateSwitching = disableRefreshRateSwitching;
+            this.baseModeRefreshRate = baseModeRefreshRate;
         }
 
         public static String priorityToString(int priority) {
             switch (priority) {
                 case PRIORITY_DEFAULT_REFRESH_RATE:
                     return "PRIORITY_DEFAULT_REFRESH_RATE";
-                case PRIORITY_FLICKER:
-                    return "PRIORITY_FLICKER";
+                case PRIORITY_FLICKER_REFRESH_RATE:
+                    return "PRIORITY_FLICKER_REFRESH_RATE";
+                case PRIORITY_FLICKER_REFRESH_RATE_SWITCH:
+                    return "PRIORITY_FLICKER_REFRESH_RATE_SWITCH";
                 case PRIORITY_USER_SETTING_MIN_REFRESH_RATE:
                     return "PRIORITY_USER_SETTING_MIN_REFRESH_RATE";
-                case PRIORITY_APP_REQUEST_REFRESH_RATE:
-                    return "PRIORITY_APP_REQUEST_REFRESH_RATE";
+                case PRIORITY_APP_REQUEST_MAX_REFRESH_RATE:
+                    return "PRIORITY_APP_REQUEST_MAX_REFRESH_RATE";
+                case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE:
+                    return "PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE";
                 case PRIORITY_APP_REQUEST_SIZE:
                     return "PRIORITY_APP_REQUEST_SIZE";
                 case PRIORITY_USER_SETTING_PEAK_REFRESH_RATE:
@@ -1018,7 +1088,9 @@
             return "Vote{"
                 + "width=" + width + ", height=" + height
                 + ", minRefreshRate=" + refreshRateRange.min
-                + ", maxRefreshRate=" + refreshRateRange.max + "}";
+                + ", maxRefreshRate=" + refreshRateRange.max
+                + ", disableRefreshRateSwitching=" + disableRefreshRateSwitching
+                + ", baseModeRefreshRate=" + baseModeRefreshRate + "}";
         }
     }
 
@@ -1182,14 +1254,17 @@
 
     final class AppRequestObserver {
         private final SparseArray<Display.Mode> mAppRequestedModeByDisplay;
+        private final SparseArray<Float> mAppPreferredMaxRefreshRateByDisplay;
 
         AppRequestObserver() {
             mAppRequestedModeByDisplay = new SparseArray<>();
+            mAppPreferredMaxRefreshRateByDisplay = new SparseArray<>();
         }
 
-        public void setAppRequestedMode(int displayId, int modeId) {
+        public void setAppRequest(int displayId, int modeId, float requestedMaxRefreshRate) {
             synchronized (mLock) {
                 setAppRequestedModeLocked(displayId, modeId);
+                setAppPreferredMaxRefreshRateLocked(displayId, requestedMaxRefreshRate);
             }
         }
 
@@ -1199,24 +1274,48 @@
                 return;
             }
 
-            final Vote refreshRateVote;
+            final Vote baseModeRefreshRateVote;
             final Vote sizeVote;
             if (requestedMode != null) {
                 mAppRequestedModeByDisplay.put(displayId, requestedMode);
-                float refreshRate = requestedMode.getRefreshRate();
-                refreshRateVote = Vote.forRefreshRates(refreshRate, refreshRate);
+                baseModeRefreshRateVote =
+                        Vote.forBaseModeRefreshRate(requestedMode.getRefreshRate());
                 sizeVote = Vote.forSize(requestedMode.getPhysicalWidth(),
                         requestedMode.getPhysicalHeight());
             } else {
                 mAppRequestedModeByDisplay.remove(displayId);
-                refreshRateVote = null;
+                baseModeRefreshRateVote = null;
                 sizeVote = null;
             }
 
-            updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE, refreshRateVote);
+            updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                    baseModeRefreshRateVote);
             updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
         }
 
+        private void setAppPreferredMaxRefreshRateLocked(int displayId,
+                float requestedMaxRefreshRate) {
+            final Vote vote;
+            final Float requestedMaxRefreshRateVote =
+                    requestedMaxRefreshRate > 0
+                            ? new Float(requestedMaxRefreshRate) : null;
+            if (Objects.equals(requestedMaxRefreshRateVote,
+                    mAppPreferredMaxRefreshRateByDisplay.get(displayId))) {
+                return;
+            }
+
+            if (requestedMaxRefreshRate > 0) {
+                mAppPreferredMaxRefreshRateByDisplay.put(displayId, requestedMaxRefreshRateVote);
+                vote = Vote.forRefreshRates(0, requestedMaxRefreshRate);
+            } else {
+                mAppPreferredMaxRefreshRateByDisplay.remove(displayId);
+                vote = null;
+            }
+            synchronized (mLock) {
+                updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE, vote);
+            }
+        }
+
         private Display.Mode findModeByIdLocked(int displayId, int modeId) {
             Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
             if (modes == null) {
@@ -1238,6 +1337,12 @@
                 final Display.Mode mode = mAppRequestedModeByDisplay.valueAt(i);
                 pw.println("    " + id + " -> " + mode);
             }
+            pw.println("    mAppPreferredMaxRefreshRateByDisplay:");
+            for (int i = 0; i < mAppPreferredMaxRefreshRateByDisplay.size(); i++) {
+                final int id = mAppPreferredMaxRefreshRateByDisplay.keyAt(i);
+                final Float refreshRate = mAppPreferredMaxRefreshRateByDisplay.valueAt(i);
+                pw.println("    " + id + " -> " + refreshRate);
+            }
         }
     }
 
@@ -1486,7 +1591,8 @@
                 updateSensorStatus();
                 if (!changeable) {
                     // Revoke previous vote from BrightnessObserver
-                    updateVoteLocked(Vote.PRIORITY_FLICKER, null);
+                    updateVoteLocked(Vote.PRIORITY_FLICKER_REFRESH_RATE, null);
+                    updateVoteLocked(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, null);
                 }
             }
         }
@@ -1734,7 +1840,8 @@
             return false;
         }
         private void onBrightnessChangedLocked() {
-            Vote vote = null;
+            Vote refreshRateVote = null;
+            Vote refreshRateSwitchingVote = null;
 
             if (mBrightness < 0) {
                 // Either the setting isn't available or we shouldn't be observing yet anyways.
@@ -1744,20 +1851,25 @@
 
             boolean insideLowZone = hasValidLowZone() && isInsideLowZone(mBrightness, mAmbientLux);
             if (insideLowZone) {
-                vote = Vote.forRefreshRates(mRefreshRateInLowZone, mRefreshRateInLowZone);
+                refreshRateVote =
+                        Vote.forRefreshRates(mRefreshRateInLowZone, mRefreshRateInLowZone);
+                refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
             }
 
             boolean insideHighZone = hasValidHighZone()
                     && isInsideHighZone(mBrightness, mAmbientLux);
             if (insideHighZone) {
-                vote = Vote.forRefreshRates(mRefreshRateInHighZone, mRefreshRateInHighZone);
+                refreshRateVote =
+                        Vote.forRefreshRates(mRefreshRateInHighZone, mRefreshRateInHighZone);
+                refreshRateSwitchingVote = Vote.forDisableRefreshRateSwitching();
             }
 
             if (mLoggingEnabled) {
                 Slog.d(TAG, "Display brightness " + mBrightness + ", ambient lux " +  mAmbientLux
-                        + ", Vote " + vote);
+                        + ", Vote " + refreshRateVote);
             }
-            updateVoteLocked(Vote.PRIORITY_FLICKER, vote);
+            updateVoteLocked(Vote.PRIORITY_FLICKER_REFRESH_RATE, refreshRateVote);
+            updateVoteLocked(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, refreshRateSwitchingVote);
         }
 
         private boolean hasValidLowZone() {
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 8800a9e..91f75e5 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -901,6 +901,14 @@
                         && preferredModeId != 0) {
                     mTmpApplySurfaceChangesTransactionState.preferredModeId = preferredModeId;
                 }
+
+                final float preferredMaxRefreshRate = getDisplayPolicy().getRefreshRatePolicy()
+                        .getPreferredMaxRefreshRate(w);
+                if (mTmpApplySurfaceChangesTransactionState.preferredMaxRefreshRate == 0
+                        && preferredMaxRefreshRate != 0) {
+                    mTmpApplySurfaceChangesTransactionState.preferredMaxRefreshRate =
+                            preferredMaxRefreshRate;
+                }
             }
         }
 
@@ -4230,6 +4238,7 @@
                     mLastHasContent,
                     mTmpApplySurfaceChangesTransactionState.preferredRefreshRate,
                     mTmpApplySurfaceChangesTransactionState.preferredModeId,
+                    mTmpApplySurfaceChangesTransactionState.preferredMaxRefreshRate,
                     mTmpApplySurfaceChangesTransactionState.preferMinimalPostProcessing,
                     true /* inTraversal, must call performTraversalInTrans... below */);
         }
@@ -4513,12 +4522,13 @@
     }
 
     private static final class ApplySurfaceChangesTransactionState {
-        boolean displayHasContent;
-        boolean obscured;
-        boolean syswin;
-        boolean preferMinimalPostProcessing;
-        float preferredRefreshRate;
-        int preferredModeId;
+        public boolean displayHasContent;
+        public boolean obscured;
+        public boolean syswin;
+        public boolean preferMinimalPostProcessing;
+        public float preferredRefreshRate;
+        public int preferredModeId;
+        public float preferredMaxRefreshRate;
 
         void reset() {
             displayHasContent = false;
@@ -4527,6 +4537,7 @@
             preferMinimalPostProcessing = false;
             preferredRefreshRate = 0;
             preferredModeId = 0;
+            preferredMaxRefreshRate = 0;
         }
     }
 
diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
index 26871d1..deaf611 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -20,6 +20,7 @@
 import static com.android.server.wm.WindowContainer.AnimationFlags.TRANSITION;
 
 import android.util.ArraySet;
+import android.view.Display;
 import android.view.Display.Mode;
 import android.view.DisplayInfo;
 
@@ -95,18 +96,10 @@
             return 0;
         }
 
-        // If app requests a certain refresh rate or mode, don't override it.
         if (w.mAttrs.preferredRefreshRate != 0 || w.mAttrs.preferredDisplayModeId != 0) {
             return w.mAttrs.preferredDisplayModeId;
         }
 
-        final String packageName = w.getOwningPackage();
-
-        // If app is using Camera, force it to default (lower) refresh rate.
-        if (mNonHighRefreshRatePackages.contains(packageName)) {
-            return mLowRefreshRateMode.getModeId();
-        }
-
         return 0;
     }
 
@@ -145,6 +138,44 @@
         if (mHighRefreshRateDenylist.isDenylisted(packageName)) {
             return mLowRefreshRateMode.getRefreshRate();
         }
+
+        final int preferredModeId = getPreferredModeId(w);
+        if (preferredModeId > 0) {
+            DisplayInfo info = w.getDisplayInfo();
+            if (info != null) {
+                for (Display.Mode mode : info.supportedModes) {
+                    if (preferredModeId == mode.getModeId()) {
+                        return mode.getRefreshRate();
+                    }
+                }
+            }
+        }
+
+        return 0;
+    }
+
+    float getPreferredMaxRefreshRate(WindowState w) {
+        // If app is animating, it's not able to control refresh rate because we want the animation
+        // to run in default refresh rate.
+        if (w.isAnimating(TRANSITION | PARENTS)) {
+            return 0;
+        }
+
+        // If app requests a certain refresh rate or mode, don't override it.
+        if (w.mAttrs.preferredDisplayModeId != 0) {
+            return 0;
+        }
+
+        if (w.mAttrs.preferredMaxDisplayRefreshRate > 0) {
+            return w.mAttrs.preferredMaxDisplayRefreshRate;
+        }
+
+        final String packageName = w.getOwningPackage();
+        // If app is using Camera, force it to default (lower) refresh rate.
+        if (mNonHighRefreshRatePackages.contains(packageName)) {
+            return mLowRefreshRateMode.getRefreshRate();
+        }
+
         return 0;
     }
 }
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index b121bfc..4eff18c 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -751,11 +751,11 @@
     int mFrameRateSelectionPriority = RefreshRatePolicy.LAYER_PRIORITY_UNSET;
 
     /**
-     * This is the frame rate which is passed to SurfaceFlinger if the window is part of the
-     * high refresh rate deny list. The variable is cached, so we do not send too many updates to
-     * SF.
+     * This is the frame rate which is passed to SurfaceFlinger if the window set a
+     * preferredDisplayModeId or is part of the high refresh rate deny list.
+     * The variable is cached, so we do not send too many updates to SF.
      */
-    float mDenyListFrameRate = 0f;
+    float mAppPreferredFrameRate = 0f;
 
     static final int BLAST_TIMEOUT_DURATION = 5000; /* milliseconds */
 
@@ -5416,10 +5416,11 @@
         }
 
         final float refreshRate = refreshRatePolicy.getPreferredRefreshRate(this);
-        if (mDenyListFrameRate != refreshRate) {
-            mDenyListFrameRate = refreshRate;
+        if (mAppPreferredFrameRate != refreshRate) {
+            mAppPreferredFrameRate = refreshRate;
             getPendingTransaction().setFrameRate(
-                    mSurfaceControl, mDenyListFrameRate, Surface.FRAME_RATE_COMPATIBILITY_EXACT);
+                    mSurfaceControl, mAppPreferredFrameRate,
+                    Surface.FRAME_RATE_COMPATIBILITY_EXACT, Surface.CHANGE_FRAME_RATE_ALWAYS);
         }
     }
 
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index cda659f..fb2db22 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -23,10 +23,11 @@
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HIGH_ZONE;
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_LOW_ZONE;
 
-import static com.android.server.display.DisplayModeDirector.Vote.PRIORITY_FLICKER;
+import static com.android.server.display.DisplayModeDirector.Vote.INVALID_SIZE;
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -137,7 +138,14 @@
             }
         }
         assertThat(defaultMode).isNotNull();
+        return createDirectorFromModeArray(modes, defaultMode);
+    }
 
+    private DisplayModeDirector createDirectorFromModeArray(Display.Mode[] modes,
+            Display.Mode defaultMode) {
+        DisplayModeDirector director =
+                new DisplayModeDirector(mContext, mHandler, mInjector);
+        director.setLoggingEnabled(true);
         SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>();
         supportedModesByDisplay.put(DISPLAY_ID, modes);
         director.injectSupportedModesByDisplay(supportedModesByDisplay);
@@ -218,8 +226,9 @@
         votesByDisplay.put(DISPLAY_ID, votes);
         float error = FLOAT_TOLERANCE / 4;
         votes.put(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE, Vote.forRefreshRates(0, 60));
-        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forRefreshRates(60 + error, 60 + error));
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE,
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE,
+                Vote.forRefreshRates(60 + error, 60 + error));
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
                 Vote.forRefreshRates(60 - error, 60 - error));
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
@@ -230,44 +239,159 @@
     }
 
     @Test
-    public void testFlickerHasLowerPriorityThanUser() {
-        assertTrue(PRIORITY_FLICKER < Vote.PRIORITY_APP_REQUEST_REFRESH_RATE);
-        assertTrue(PRIORITY_FLICKER < Vote.PRIORITY_APP_REQUEST_SIZE);
+    public void testFlickerHasLowerPriorityThanUserAndRangeIsSingle() {
+        assertTrue(Vote.PRIORITY_FLICKER_REFRESH_RATE
+                < Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertTrue(Vote.PRIORITY_FLICKER_REFRESH_RATE
+                < Vote.PRIORITY_APP_REQUEST_SIZE);
 
-        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+        assertTrue(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH
+                > Vote.PRIORITY_LOW_POWER_MODE);
+
+        Display.Mode[] modes = new Display.Mode[4];
+        modes[0] = new Display.Mode(
+                /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
+        modes[1] = new Display.Mode(
+                /*modeId=*/2, /*width=*/2000, /*height=*/2000, 60);
+        modes[2] = new Display.Mode(
+                /*modeId=*/3, /*width=*/1000, /*height=*/1000, 90);
+        modes[3] = new Display.Mode(
+                /*modeId=*/4, /*width=*/2000, /*height=*/2000, 90);
+
+        DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE, Vote.forRefreshRates(60, 90));
-        votes.put(PRIORITY_FLICKER, Vote.forRefreshRates(60, 60));
+        Display.Mode appRequestedMode = modes[1];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(2);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+
+        votes.clear();
+        appRequestedMode = modes[3];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(4);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+
+        votes.clear();
+        appRequestedMode = modes[3];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(4);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+
+        votes.clear();
+        appRequestedMode = modes[1];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(90, 90));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(2);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max)
+                .isWithin(FLOAT_TOLERANCE).of(desiredSpecs.primaryRefreshRateRange.min);
+    }
+
+    @Test
+    public void testLPMHasHigherPriorityThanUser() {
+        assertTrue(Vote.PRIORITY_LOW_POWER_MODE > Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertTrue(Vote.PRIORITY_LOW_POWER_MODE > Vote.PRIORITY_APP_REQUEST_SIZE);
+
+
+        Display.Mode[] modes = new Display.Mode[4];
+        modes[0] = new Display.Mode(
+                /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
+        modes[1] = new Display.Mode(
+                /*modeId=*/2, /*width=*/2000, /*height=*/2000, 60);
+        modes[2] = new Display.Mode(
+                /*modeId=*/3, /*width=*/1000, /*height=*/1000, 90);
+        modes[3] = new Display.Mode(
+                /*modeId=*/4, /*width=*/2000, /*height=*/2000, 90);
+
+        DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        Display.Mode appRequestedMode = modes[1];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(60, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(2);
         assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
 
         votes.clear();
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE, Vote.forRefreshRates(60, 90));
-        votes.put(PRIORITY_FLICKER, Vote.forRefreshRates(90, 90));
+        appRequestedMode = modes[3];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(90, 90));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(4);
         assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
 
         votes.clear();
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE, Vote.forRefreshRates(90, 90));
-        votes.put(PRIORITY_FLICKER, Vote.forRefreshRates(60, 60));
+        appRequestedMode = modes[3];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(60, 60));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
-
-        votes.clear();
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE, Vote.forRefreshRates(60, 60));
-        votes.put(PRIORITY_FLICKER, Vote.forRefreshRates(90, 90));
-        director.injectVotesByDisplay(votesByDisplay);
-        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(2);
         assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+
+        votes.clear();
+        appRequestedMode = modes[1];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(90, 90));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(4);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
     }
 
     @Test
@@ -275,19 +399,30 @@
         // Confirm that the app request range doesn't include flicker or min refresh rate settings,
         // but does include everything else.
         assertTrue(
-                PRIORITY_FLICKER < Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
+                Vote.PRIORITY_FLICKER_REFRESH_RATE
+                        < Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
         assertTrue(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE
                 < Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
-        assertTrue(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE
+        assertTrue(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE
                 >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
 
-        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+        Display.Mode[] modes = new Display.Mode[3];
+        modes[0] = new Display.Mode(
+                /*modeId=*/60, /*width=*/1000, /*height=*/1000, 60);
+        modes[1] = new Display.Mode(
+                /*modeId=*/75, /*width=*/2000, /*height=*/2000, 75);
+        modes[2] = new Display.Mode(
+                /*modeId=*/90, /*width=*/1000, /*height=*/1000, 90);
+
+        DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(PRIORITY_FLICKER, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
         director.injectVotesByDisplay(votesByDisplay);
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(60);
         assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
         assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
@@ -297,22 +432,24 @@
                 Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(90);
         assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
         assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90f);
         assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
         assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
 
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE, Vote.forRefreshRates(75, 75));
+        Display.Mode appRequestedMode = modes[1];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(75);
         assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
         assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(75);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.min)
-                .isWithin(FLOAT_TOLERANCE)
-                .of(75);
-        assertThat(desiredSpecs.appRequestRefreshRateRange.max)
-                .isWithin(FLOAT_TOLERANCE)
-                .of(75);
+        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
     }
 
     void verifySpecsWithRefreshRateSettings(DisplayModeDirector director, float minFps,
@@ -374,18 +511,29 @@
 
     @Test
     public void testVotingWithAlwaysRespectAppRequest() {
-        DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
+        Display.Mode[] modes = new Display.Mode[3];
+        modes[0] = new Display.Mode(
+                /*modeId=*/50, /*width=*/1000, /*height=*/1000, 50);
+        modes[1] = new Display.Mode(
+                /*modeId=*/60, /*width=*/1000, /*height=*/1000, 60);
+        modes[2] = new Display.Mode(
+                /*modeId=*/90, /*width=*/1000, /*height=*/1000, 90);
+
+        DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
+
+
         SparseArray<Vote> votes = new SparseArray<>();
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
-        votes.put(PRIORITY_FLICKER, Vote.forRefreshRates(0, 60));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(0, 60));
         votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE, Vote.forRefreshRates(60, 90));
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE, Vote.forRefreshRates(90, 90));
+        Display.Mode appRequestedMode = modes[2];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
         votes.put(Vote.PRIORITY_USER_SETTING_PEAK_REFRESH_RATE, Vote.forRefreshRates(60, 60));
         votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
-
-
         director.injectVotesByDisplay(votesByDisplay);
+
         assertThat(director.shouldAlwaysRespectAppRequestedMode()).isFalse();
         DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
 
@@ -396,8 +544,8 @@
         director.setShouldAlwaysRespectAppRequestedMode(true);
         assertThat(director.shouldAlwaysRespectAppRequestedMode()).isTrue();
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
-        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
-        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isAtMost(50);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90);
         assertThat(desiredSpecs.baseModeId).isEqualTo(90);
 
         director.setShouldAlwaysRespectAppRequestedMode(false);
@@ -497,7 +645,8 @@
         // Inject votes
         SparseArray<Vote> votes = new SparseArray<>();
         votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(1920, 1080));
-        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE, Vote.forRefreshRates(50, 50));
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(60));
         SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
         votesByDisplay.put(DISPLAY_ID, votes);
         director.injectVotesByDisplay(votesByDisplay);
@@ -592,14 +741,19 @@
         // Sensor reads 20 lux,
         listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 20 /*lux*/));
 
-        Vote vote = director.getVote(Display.DEFAULT_DISPLAY, PRIORITY_FLICKER);
+        Vote vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
         assertVoteForRefreshRate(vote, 90 /*fps*/);
+        vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
+        assertThat(vote).isNotNull();
+        assertThat(vote.disableRefreshRateSwitching).isTrue();
 
         setBrightness(125);
         // Sensor reads 1000 lux,
         listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 1000 /*lux*/));
 
-        vote = director.getVote(Display.DEFAULT_DISPLAY, PRIORITY_FLICKER);
+        vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
+        assertThat(vote).isNull();
+        vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNull();
     }
 
@@ -635,15 +789,20 @@
         // Sensor reads 2000 lux,
         listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 2000));
 
-        Vote vote = director.getVote(Display.DEFAULT_DISPLAY, PRIORITY_FLICKER);
+        Vote vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
+        assertThat(vote).isNull();
+        vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
         assertThat(vote).isNull();
 
         setBrightness(255);
         // Sensor reads 9000 lux,
         listener.onSensorChanged(TestUtils.createSensorEvent(lightSensor, 9000));
 
-        vote = director.getVote(Display.DEFAULT_DISPLAY, PRIORITY_FLICKER);
+        vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE);
         assertVoteForRefreshRate(vote, 60 /*fps*/);
+        vote = director.getVote(Display.DEFAULT_DISPLAY, Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH);
+        assertThat(vote).isNotNull();
+        assertThat(vote.disableRefreshRateSwitching).isTrue();
     }
 
     @Test
@@ -724,6 +883,336 @@
         assertNull(vote);
     }
 
+    @Test
+    public void testAppRequestMaxRefreshRate() {
+        // Confirm that the app max request range doesn't include flicker or min refresh rate
+        // settings but does include everything else.
+        assertTrue(Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE
+                >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
+
+        Display.Mode[] modes = new Display.Mode[3];
+        modes[0] = new Display.Mode(
+                /*modeId=*/60, /*width=*/1000, /*height=*/1000, 60);
+        modes[1] = new Display.Mode(
+                /*modeId=*/75, /*width=*/1000, /*height=*/1000, 75);
+        modes[2] = new Display.Mode(
+                /*modeId=*/90, /*width=*/1000, /*height=*/1000, 90);
+
+        DisplayModeDirector director = createDirectorFromModeArray(modes, modes[1]);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE, Vote.forRefreshRates(60, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
+                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90f);
+        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isAtMost(60f);
+        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+
+        votes.put(Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE, Vote.forRefreshRates(0, 75));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isZero();
+        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(75);
+    }
+
+    @Test
+    public void testAppRequestObserver_modeId() {
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 0);
+
+        Vote appRequestRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertNotNull(appRequestRefreshRate);
+        assertThat(appRequestRefreshRate.refreshRateRange.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
+        assertThat(appRequestRefreshRate.baseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
+
+        Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+        assertNotNull(appRequestSize);
+        assertThat(appRequestSize.refreshRateRange.min).isZero();
+        assertThat(appRequestSize.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestSize.disableRefreshRateSwitching).isFalse();
+        assertThat(appRequestSize.baseModeRefreshRate).isZero();
+        assertThat(appRequestSize.height).isEqualTo(1000);
+        assertThat(appRequestSize.width).isEqualTo(1000);
+
+        Vote appRequestMaxRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE);
+        assertNull(appRequestMaxRefreshRate);
+
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 90, 0);
+
+        appRequestRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertNotNull(appRequestRefreshRate);
+        assertThat(appRequestRefreshRate.refreshRateRange.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
+        assertThat(appRequestRefreshRate.baseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
+
+        appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+        assertNotNull(appRequestSize);
+        assertThat(appRequestSize.refreshRateRange.min).isZero();
+        assertThat(appRequestSize.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestSize.height).isEqualTo(1000);
+        assertThat(appRequestSize.width).isEqualTo(1000);
+
+        appRequestMaxRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE);
+        assertNull(appRequestMaxRefreshRate);
+    }
+
+    @Test
+    public void testAppRequestObserver_maxRefreshRate() {
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 90);
+        Vote appRequestRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertNull(appRequestRefreshRate);
+
+        Vote appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+        assertNull(appRequestSize);
+
+        Vote appRequestMaxRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE);
+        assertNotNull(appRequestMaxRefreshRate);
+        assertThat(appRequestMaxRefreshRate.refreshRateRange.min).isZero();
+        assertThat(appRequestMaxRefreshRate.refreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(appRequestMaxRefreshRate.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestMaxRefreshRate.width).isEqualTo(INVALID_SIZE);
+
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 60);
+        appRequestRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertNull(appRequestRefreshRate);
+
+        appRequestSize = director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+        assertNull(appRequestSize);
+
+        appRequestMaxRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE);
+        assertNotNull(appRequestMaxRefreshRate);
+        assertThat(appRequestMaxRefreshRate.refreshRateRange.min).isZero();
+        assertThat(appRequestMaxRefreshRate.refreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(appRequestMaxRefreshRate.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestMaxRefreshRate.width).isEqualTo(INVALID_SIZE);
+    }
+
+    @Test
+    public void testAppRequestObserver_modeIdAndMaxRefreshRate() {
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 90);
+
+        Vote appRequestRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
+        assertNotNull(appRequestRefreshRate);
+        assertThat(appRequestRefreshRate.refreshRateRange.min).isZero();
+        assertThat(appRequestRefreshRate.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestRefreshRate.disableRefreshRateSwitching).isFalse();
+        assertThat(appRequestRefreshRate.baseModeRefreshRate).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(appRequestRefreshRate.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestRefreshRate.width).isEqualTo(INVALID_SIZE);
+
+        Vote appRequestSize =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_SIZE);
+        assertNotNull(appRequestSize);
+        assertThat(appRequestSize.refreshRateRange.min).isZero();
+        assertThat(appRequestSize.refreshRateRange.max).isPositiveInfinity();
+        assertThat(appRequestSize.height).isEqualTo(1000);
+        assertThat(appRequestSize.width).isEqualTo(1000);
+
+        Vote appRequestMaxRefreshRate =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE);
+        assertNotNull(appRequestMaxRefreshRate);
+        assertThat(appRequestMaxRefreshRate.refreshRateRange.min).isZero();
+        assertThat(appRequestMaxRefreshRate.refreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(appRequestMaxRefreshRate.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestMaxRefreshRate.width).isEqualTo(INVALID_SIZE);
+    }
+
+    @Test
+    public void testAppRequestsIsTheDefaultMode() {
+        Display.Mode[] modes = new Display.Mode[2];
+        modes[0] = new Display.Mode(
+                /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
+        modes[1] = new Display.Mode(
+                /*modeId=*/2, /*width=*/1000, /*height=*/1000, 90);
+
+        DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(1);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isAtMost(60);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90);
+
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        Display.Mode appRequestedMode = modes[1];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                        appRequestedMode.getPhysicalHeight()));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(2);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isAtMost(60);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isAtLeast(90);
+    }
+
+    @Test
+    public void testDisableRefreshRateSwitchingVote() {
+        DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
+                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(50);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(50);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(50);
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE,
+                Vote.forRefreshRates(70, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
+                Vote.forRefreshRates(80, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 90));
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(80);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(80);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(80);
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE,
+                Vote.forRefreshRates(90, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_USER_SETTING_MIN_REFRESH_RATE,
+                Vote.forRefreshRates(80, Float.POSITIVE_INFINITY));
+        votes.put(Vote.PRIORITY_FLICKER_REFRESH_RATE_SWITCH, Vote.forDisableRefreshRateSwitching());
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 90));
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(90);
+    }
+
+    @Test
+    public void testBaseModeIdInPrimaryRange() {
+        DisplayModeDirector director = createDirectorFromFpsRange(50, 90);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(70));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(50);
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(55));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(55);
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE, Vote.forRefreshRates(0, 52));
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(55));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(55);
+
+        votes.clear();
+        votes.put(Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE, Vote.forRefreshRates(0, 58));
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(55));
+        votes.put(Vote.PRIORITY_LOW_POWER_MODE, Vote.forRefreshRates(0, 60));
+        director.injectVotesByDisplay(votesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(0);
+        assertThat(desiredSpecs.primaryRefreshRateRange.max).isWithin(FLOAT_TOLERANCE).of(58);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(55);
+    }
+
+    @Test
+    public void testStaleAppVote() {
+        Display.Mode[] modes = new Display.Mode[4];
+        modes[0] = new Display.Mode(
+                /*modeId=*/1, /*width=*/1000, /*height=*/1000, 60);
+        modes[1] = new Display.Mode(
+                /*modeId=*/2, /*width=*/2000, /*height=*/2000, 60);
+        modes[2] = new Display.Mode(
+                /*modeId=*/3, /*width=*/1000, /*height=*/1000, 90);
+        modes[3] = new Display.Mode(
+                /*modeId=*/4, /*width=*/2000, /*height=*/2000, 90);
+
+        DisplayModeDirector director = createDirectorFromModeArray(modes, modes[0]);
+        SparseArray<Vote> votes = new SparseArray<>();
+        SparseArray<SparseArray<Vote>> votesByDisplay = new SparseArray<>();
+        votesByDisplay.put(DISPLAY_ID, votes);
+        Display.Mode appRequestedMode = modes[3];
+        votes.put(Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE,
+                Vote.forBaseModeRefreshRate(appRequestedMode.getRefreshRate()));
+        votes.put(Vote.PRIORITY_APP_REQUEST_SIZE, Vote.forSize(appRequestedMode.getPhysicalWidth(),
+                appRequestedMode.getPhysicalHeight()));
+        director.injectVotesByDisplay(votesByDisplay);
+        DesiredDisplayModeSpecs desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(4);
+
+        // Change mode Id's to simulate that a hotplug has occurred.
+        Display.Mode[] newModes = new Display.Mode[4];
+        newModes[0] = new Display.Mode(
+                /*modeId=*/5, /*width=*/1000, /*height=*/1000, 60);
+        newModes[1] = new Display.Mode(
+                /*modeId=*/6, /*width=*/2000, /*height=*/2000, 60);
+        newModes[2] = new Display.Mode(
+                /*modeId=*/7, /*width=*/1000, /*height=*/1000, 90);
+        newModes[3] = new Display.Mode(
+                /*modeId=*/8, /*width=*/2000, /*height=*/2000, 90);
+
+        SparseArray<Display.Mode[]> supportedModesByDisplay = new SparseArray<>();
+        supportedModesByDisplay.put(DISPLAY_ID, newModes);
+        director.injectSupportedModesByDisplay(supportedModesByDisplay);
+        SparseArray<Display.Mode> defaultModesByDisplay = new SparseArray<>();
+        defaultModesByDisplay.put(DISPLAY_ID, newModes[0]);
+        director.injectDefaultModeByDisplay(defaultModesByDisplay);
+        desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
+        assertThat(desiredSpecs.baseModeId).isEqualTo(8);
+    }
+
     private void assertVoteForRefreshRate(Vote vote, float refreshRate) {
         assertThat(vote).isNotNull();
         final DisplayModeDirector.RefreshRateRange expectedRange =
diff --git a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java
index 325bca4..1ee646c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/FrameRateSelectionPriorityTests.java
@@ -77,12 +77,12 @@
         assertNotNull("Window state is created", appWindow);
 
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority doesn't change.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         // Call the function a few times.
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
@@ -92,7 +92,7 @@
         verify(appWindow.getPendingTransaction(), never()).setFrameRateSelectionPriority(
                 any(SurfaceControl.class), anyInt());
         verify(appWindow.getPendingTransaction(), never()).setFrameRate(
-                any(SurfaceControl.class), anyInt(), anyInt());
+                any(SurfaceControl.class), anyInt(), anyInt(), anyInt());
     }
 
     @Test
@@ -101,16 +101,16 @@
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
         assertEquals(appWindow.getDisplayContent().getDisplayPolicy().getRefreshRatePolicy()
                 .getPreferredModeId(appWindow), 0);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
         assertEquals(appWindow.getDisplayContent().getDisplayPolicy().getRefreshRatePolicy()
                 .getPreferredRefreshRate(appWindow), 0, FLOAT_TOLERANCE);
 
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority stays MAX_VALUE.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
         verify(appWindow.getPendingTransaction(), never()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET);
 
@@ -119,38 +119,38 @@
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes to 1.
         assertEquals(appWindow.mFrameRateSelectionPriority, 1);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
         verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), 1);
         verify(appWindow.getPendingTransaction(), never()).setFrameRate(
-                any(SurfaceControl.class), anyInt(), anyInt());
+                any(SurfaceControl.class), anyInt(), anyInt(), anyInt());
     }
 
     @Test
     public void testApplicationInFocusWithModeId() {
         final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         // Application is in focus.
         appWindow.mToken.mDisplayContent.mCurrentFocus = appWindow;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes.
         assertEquals(appWindow.mFrameRateSelectionPriority, 1);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
         // Update the mode ID to a requested number.
         appWindow.mAttrs.preferredDisplayModeId = 1;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes.
         assertEquals(appWindow.mFrameRateSelectionPriority, 0);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 60, FLOAT_TOLERANCE);
 
         // Remove the mode ID request.
         appWindow.mAttrs.preferredDisplayModeId = 0;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes.
         assertEquals(appWindow.mFrameRateSelectionPriority, 1);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         // Verify we called actions on Transactions correctly.
         verify(appWindow.getPendingTransaction(), never()).setFrameRateSelectionPriority(
@@ -160,14 +160,14 @@
         verify(appWindow.getPendingTransaction(), times(2)).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), 1);
         verify(appWindow.getPendingTransaction(), never()).setFrameRate(
-                any(SurfaceControl.class), anyInt(), anyInt());
+                any(SurfaceControl.class), anyInt(), anyInt(), anyInt());
     }
 
     @Test
     public void testApplicationNotInFocusWithModeId() {
         final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         final WindowState inFocusWindow = createWindow(null, TYPE_APPLICATION, "inFocus");
         appWindow.mToken.mDisplayContent.mCurrentFocus = inFocusWindow;
@@ -175,28 +175,28 @@
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // The window is not in focus.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         // Update the mode ID to a requested number.
         appWindow.mAttrs.preferredDisplayModeId = 1;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority changes.
         assertEquals(appWindow.mFrameRateSelectionPriority, 2);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 60, FLOAT_TOLERANCE);
 
         verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET);
         verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), 2);
         verify(appWindow.getPendingTransaction(), never()).setFrameRate(
-                any(SurfaceControl.class), anyInt(), anyInt());
+                any(SurfaceControl.class), anyInt(), anyInt(), anyInt());
     }
 
     @Test
     public void testApplicationNotInFocusWithoutModeId() {
         final WindowState appWindow = createWindow(null, TYPE_APPLICATION, "appWindow");
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         final WindowState inFocusWindow = createWindow(null, TYPE_APPLICATION, "inFocus");
         appWindow.mToken.mDisplayContent.mCurrentFocus = inFocusWindow;
@@ -204,19 +204,19 @@
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // The window is not in focus.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         // Make sure that the mode ID is not set.
         appWindow.mAttrs.preferredDisplayModeId = 0;
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         // Priority doesn't change.
         assertEquals(appWindow.mFrameRateSelectionPriority, RefreshRatePolicy.LAYER_PRIORITY_UNSET);
-        assertEquals(appWindow.mDenyListFrameRate, 0, FLOAT_TOLERANCE);
+        assertEquals(appWindow.mAppPreferredFrameRate, 0, FLOAT_TOLERANCE);
 
         verify(appWindow.getPendingTransaction()).setFrameRateSelectionPriority(
                 appWindow.getSurfaceControl(), RefreshRatePolicy.LAYER_PRIORITY_UNSET);
         verify(appWindow.getPendingTransaction(), never()).setFrameRate(
-                any(SurfaceControl.class), anyInt(), anyInt());
+                any(SurfaceControl.class), anyInt(), anyInt(), anyInt());
     }
 
     @Test
@@ -233,7 +233,7 @@
 
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
         assertEquals(RefreshRatePolicy.LAYER_PRIORITY_UNSET, appWindow.mFrameRateSelectionPriority);
-        assertEquals(60, appWindow.mDenyListFrameRate, FLOAT_TOLERANCE);
+        assertEquals(60, appWindow.mAppPreferredFrameRate, FLOAT_TOLERANCE);
 
         // Call the function a few times.
         appWindow.updateFrameRateSelectionPriorityIfNeeded();
@@ -243,6 +243,7 @@
         verify(appWindow.getPendingTransaction(), never()).setFrameRateSelectionPriority(
                 any(SurfaceControl.class), anyInt());
         verify(appWindow.getPendingTransaction(), times(1)).setFrameRate(
-                appWindow.getSurfaceControl(), 60, Surface.FRAME_RATE_COMPATIBILITY_EXACT);
+                appWindow.getSurfaceControl(), 60,
+                Surface.FRAME_RATE_COMPATIBILITY_EXACT, Surface.CHANGE_FRAME_RATE_ALWAYS);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index ef3c7ae..20b987d 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -71,9 +71,11 @@
         cameraUsingWindow.mAttrs.packageName = "com.android.test";
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.addNonHighRefreshRatePackage("com.android.test");
-        assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(cameraUsingWindow));
+        assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertEquals(60, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.removeNonHighRefreshRatePackage("com.android.test");
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
@@ -109,6 +111,7 @@
         mPolicy.addNonHighRefreshRatePackage("com.android.test");
         assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
     }
 
     @Test
@@ -123,6 +126,7 @@
         mPolicy.addNonHighRefreshRatePackage("com.android.test");
         assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
     }
 
     @Test
@@ -132,13 +136,31 @@
         cameraUsingWindow.mAttrs.packageName = "com.android.test";
 
         mPolicy.addNonHighRefreshRatePackage("com.android.test");
-        assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(cameraUsingWindow));
+        assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertEquals(60, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
 
         cameraUsingWindow.mActivityRecord.mSurfaceAnimator.startAnimation(
                 cameraUsingWindow.getPendingTransaction(), mock(AnimationAdapter.class),
                 false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+    }
+
+    @Test
+    public void testAppMaxRefreshRate() {
+        final WindowState window = createWindow(null, TYPE_BASE_APPLICATION, "window");
+        window.mAttrs.preferredMaxDisplayRefreshRate = 60f;
+        assertEquals(0, mPolicy.getPreferredModeId(window));
+        assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertEquals(60, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
+
+        window.mActivityRecord.mSurfaceAnimator.startAnimation(
+                window.getPendingTransaction(), mock(AnimationAdapter.class),
+                false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
+        assertEquals(0, mPolicy.getPreferredModeId(window));
+        assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
     }
 }
diff --git a/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java b/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java
index 619aee6..b89539c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java
+++ b/services/tests/wmtests/src/com/android/server/wm/StubTransaction.java
@@ -231,7 +231,7 @@
 
     @Override
     public SurfaceControl.Transaction setFrameRate(SurfaceControl sc, float frameRate,
-            int compatibility) {
+            int compatibility, int changeFrameRateStrategy) {
         return this;
     }