Merge "Add preferredMinDisplayRefreshRate" into sc-dev
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 1c0ae28..abcc33c 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -207,6 +207,8 @@
      * has a preference.
      * @param requestedModeId The preferred mode id for the top-most visible window that has a
      * preference.
+     * @param requestedMinRefreshRate The preferred lowest refresh rate 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
@@ -216,8 +218,9 @@
      * prior to call to performTraversalInTransactionFromWindowManager.
      */
     public abstract void setDisplayProperties(int displayId, boolean hasContent,
-            float requestedRefreshRate, int requestedModeId, float requestedMaxRefreshRate,
-            boolean requestedMinimalPostProcessing, boolean inTraversal);
+            float requestedRefreshRate, int requestedModeId, float requestedMinRefreshRate,
+            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 2996c3d..55beae0f 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -3023,6 +3023,14 @@
         public int preferredDisplayModeId;
 
         /**
+         * The min display refresh rate while the window is in focus.
+         *
+         * This value is ignored if {@link #preferredDisplayModeId} is set.
+         * @hide
+         */
+        public float preferredMinDisplayRefreshRate;
+
+        /**
          * The max display refresh rate while the window is in focus.
          *
          * This value is ignored if {@link #preferredDisplayModeId} is set.
@@ -3781,6 +3789,7 @@
             out.writeInt(screenOrientation);
             out.writeFloat(preferredRefreshRate);
             out.writeInt(preferredDisplayModeId);
+            out.writeFloat(preferredMinDisplayRefreshRate);
             out.writeFloat(preferredMaxDisplayRefreshRate);
             out.writeInt(systemUiVisibility);
             out.writeInt(subtreeSystemUiVisibility);
@@ -3852,6 +3861,7 @@
             screenOrientation = in.readInt();
             preferredRefreshRate = in.readFloat();
             preferredDisplayModeId = in.readInt();
+            preferredMinDisplayRefreshRate = in.readFloat();
             preferredMaxDisplayRefreshRate = in.readFloat();
             systemUiVisibility = in.readInt();
             subtreeSystemUiVisibility = in.readInt();
@@ -3931,7 +3941,9 @@
         /** {@hide} */
         public static final int BLUR_BEHIND_RADIUS_CHANGED = 1 << 29;
         /** {@hide} */
-        public static final int PREFERRED_MAX_DISPLAY_REFRESH_RATE = 1 << 30;
+        public static final int PREFERRED_MIN_DISPLAY_REFRESH_RATE = 1 << 30;
+        /** {@hide} */
+        public static final int PREFERRED_MAX_DISPLAY_REFRESH_RATE = 1 << 31;
 
         // internal buffer to backup/restore parameters under compatibility mode.
         private int[] mCompatibilityParamsBackup = null;
@@ -4063,6 +4075,11 @@
                 changes |= PREFERRED_DISPLAY_MODE_ID;
             }
 
+            if (preferredMinDisplayRefreshRate != o.preferredMinDisplayRefreshRate) {
+                preferredMinDisplayRefreshRate = o.preferredMinDisplayRefreshRate;
+                changes |= PREFERRED_MIN_DISPLAY_REFRESH_RATE;
+            }
+
             if (preferredMaxDisplayRefreshRate != o.preferredMaxDisplayRefreshRate) {
                 preferredMaxDisplayRefreshRate = o.preferredMaxDisplayRefreshRate;
                 changes |= PREFERRED_MAX_DISPLAY_REFRESH_RATE;
@@ -4272,6 +4289,10 @@
                 sb.append(" preferredDisplayMode=");
                 sb.append(preferredDisplayModeId);
             }
+            if (preferredMinDisplayRefreshRate != 0) {
+                sb.append(" preferredMinDisplayRefreshRate=");
+                sb.append(preferredMinDisplayRefreshRate);
+            }
             if (preferredMaxDisplayRefreshRate != 0) {
                 sb.append(" preferredMaxDisplayRefreshRate=");
                 sb.append(preferredMaxDisplayRefreshRate);
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 0decd33..79ea108 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1506,8 +1506,9 @@
     }
 
     private void setDisplayPropertiesInternal(int displayId, boolean hasContent,
-            float requestedRefreshRate, int requestedModeId, float requestedMaxRefreshRate,
-            boolean preferMinimalPostProcessing, boolean inTraversal) {
+            float requestedRefreshRate, int requestedModeId, float requestedMinRefreshRate,
+            float requestedMaxRefreshRate, boolean preferMinimalPostProcessing,
+            boolean inTraversal) {
         synchronized (mSyncRoot) {
             final LogicalDisplay display = mLogicalDisplayMapper.getDisplayLocked(displayId);
             if (display == null) {
@@ -1538,7 +1539,7 @@
                 }
             }
             mDisplayModeDirector.getAppRequestObserver().setAppRequest(
-                    displayId, requestedModeId, requestedMaxRefreshRate);
+                    displayId, requestedModeId, requestedMinRefreshRate, requestedMaxRefreshRate);
 
             if (display.getDisplayInfoLocked().minimalPostProcessingSupported) {
                 boolean mppRequest = mMinimalPostProcessingAllowed && preferMinimalPostProcessing;
@@ -3208,11 +3209,12 @@
 
         @Override
         public void setDisplayProperties(int displayId, boolean hasContent,
-                float requestedRefreshRate, int requestedMode, float requestedMaxRefreshRate,
-                boolean requestedMinimalPostProcessing, boolean inTraversal) {
+                float requestedRefreshRate, int requestedMode, float requestedMinRefreshRate,
+                float requestedMaxRefreshRate, boolean requestedMinimalPostProcessing,
+                boolean inTraversal) {
             setDisplayPropertiesInternal(displayId, hasContent, requestedRefreshRate,
-                    requestedMode, requestedMaxRefreshRate, requestedMinimalPostProcessing,
-                    inTraversal);
+                    requestedMode, requestedMinRefreshRate, 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 83fc966..f23ae6e 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -913,10 +913,12 @@
         // 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
+        // APP_REQUEST_REFRESH_RATE_RANGE 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;
+        // @see android.view.WindowManager.LayoutParams#preferredMinRefreshRate
+        // @see android.view.WindowManager.LayoutParams#preferredMaxRefreshRate
+        // It votes to [preferredMinRefreshRate, preferredMaxRefreshRate].
+        public static final int PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE = 3;
 
         // We split the app request into different priorities in case we can satisfy one desire
         // without the other.
@@ -967,7 +969,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_MAX_REFRESH_RATE;
+                PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE;
 
         /**
          * A value signifying an invalid width or height in a vote.
@@ -1035,8 +1037,8 @@
             switch (priority) {
                 case PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE:
                     return "PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE";
-                case PRIORITY_APP_REQUEST_MAX_REFRESH_RATE:
-                    return "PRIORITY_APP_REQUEST_MAX_REFRESH_RATE";
+                case PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE:
+                    return "PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE";
                 case PRIORITY_APP_REQUEST_SIZE:
                     return "PRIORITY_APP_REQUEST_SIZE";
                 case PRIORITY_DEFAULT_REFRESH_RATE:
@@ -1233,17 +1235,19 @@
 
     final class AppRequestObserver {
         private final SparseArray<Display.Mode> mAppRequestedModeByDisplay;
-        private final SparseArray<Float> mAppPreferredMaxRefreshRateByDisplay;
+        private final SparseArray<RefreshRateRange> mAppPreferredRefreshRateRangeByDisplay;
 
         AppRequestObserver() {
             mAppRequestedModeByDisplay = new SparseArray<>();
-            mAppPreferredMaxRefreshRateByDisplay = new SparseArray<>();
+            mAppPreferredRefreshRateRangeByDisplay = new SparseArray<>();
         }
 
-        public void setAppRequest(int displayId, int modeId, float requestedMaxRefreshRate) {
+        public void setAppRequest(int displayId, int modeId, float requestedMinRefreshRateRange,
+                float requestedMaxRefreshRateRange) {
             synchronized (mLock) {
                 setAppRequestedModeLocked(displayId, modeId);
-                setAppPreferredMaxRefreshRateLocked(displayId, requestedMaxRefreshRate);
+                setAppPreferredRefreshRateRangeLocked(displayId, requestedMinRefreshRateRange,
+                        requestedMaxRefreshRateRange);
             }
         }
 
@@ -1272,26 +1276,36 @@
             updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_SIZE, sizeVote);
         }
 
-        private void setAppPreferredMaxRefreshRateLocked(int displayId,
-                float requestedMaxRefreshRate) {
+        private void setAppPreferredRefreshRateRangeLocked(int displayId,
+                float requestedMinRefreshRateRange, float requestedMaxRefreshRateRange) {
             final Vote vote;
-            final Float requestedMaxRefreshRateVote =
-                    requestedMaxRefreshRate > 0
-                            ? new Float(requestedMaxRefreshRate) : null;
-            if (Objects.equals(requestedMaxRefreshRateVote,
-                    mAppPreferredMaxRefreshRateByDisplay.get(displayId))) {
+
+            RefreshRateRange refreshRateRange = null;
+            if (requestedMinRefreshRateRange > 0 || requestedMaxRefreshRateRange > 0) {
+                float min = requestedMinRefreshRateRange;
+                float max = requestedMaxRefreshRateRange > 0
+                        ? requestedMaxRefreshRateRange : Float.POSITIVE_INFINITY;
+                refreshRateRange = new RefreshRateRange(min, max);
+                if (refreshRateRange.min == 0 && refreshRateRange.max == 0) {
+                    // requestedMinRefreshRateRange/requestedMaxRefreshRateRange were invalid
+                    refreshRateRange = null;
+                }
+            }
+
+            if (Objects.equals(refreshRateRange,
+                    mAppPreferredRefreshRateRangeByDisplay.get(displayId))) {
                 return;
             }
 
-            if (requestedMaxRefreshRate > 0) {
-                mAppPreferredMaxRefreshRateByDisplay.put(displayId, requestedMaxRefreshRateVote);
-                vote = Vote.forRefreshRates(0, requestedMaxRefreshRate);
+            if (refreshRateRange != null) {
+                mAppPreferredRefreshRateRangeByDisplay.put(displayId, refreshRateRange);
+                vote = Vote.forRefreshRates(refreshRateRange.min, refreshRateRange.max);
             } else {
-                mAppPreferredMaxRefreshRateByDisplay.remove(displayId);
+                mAppPreferredRefreshRateRangeByDisplay.remove(displayId);
                 vote = null;
             }
             synchronized (mLock) {
-                updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE, vote);
+                updateVoteLocked(displayId, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE, vote);
             }
         }
 
@@ -1316,11 +1330,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);
+            pw.println("    mAppPreferredRefreshRateRangeByDisplay:");
+            for (int i = 0; i < mAppPreferredRefreshRateRangeByDisplay.size(); i++) {
+                final int id = mAppPreferredRefreshRateRangeByDisplay.keyAt(i);
+                final RefreshRateRange refreshRateRange =
+                        mAppPreferredRefreshRateRangeByDisplay.valueAt(i);
+                pw.println("    " + id + " -> " + refreshRateRange);
             }
         }
     }
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index 62d8ace..7d9971c 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -914,6 +914,14 @@
                     mTmpApplySurfaceChangesTransactionState.preferredModeId = preferredModeId;
                 }
 
+                final float preferredMinRefreshRate = getDisplayPolicy().getRefreshRatePolicy()
+                        .getPreferredMinRefreshRate(w);
+                if (mTmpApplySurfaceChangesTransactionState.preferredMinRefreshRate == 0
+                        && preferredMinRefreshRate != 0) {
+                    mTmpApplySurfaceChangesTransactionState.preferredMinRefreshRate =
+                            preferredMinRefreshRate;
+                }
+
                 final float preferredMaxRefreshRate = getDisplayPolicy().getRefreshRatePolicy()
                         .getPreferredMaxRefreshRate(w);
                 if (mTmpApplySurfaceChangesTransactionState.preferredMaxRefreshRate == 0
@@ -4321,6 +4329,7 @@
                     mLastHasContent,
                     mTmpApplySurfaceChangesTransactionState.preferredRefreshRate,
                     mTmpApplySurfaceChangesTransactionState.preferredModeId,
+                    mTmpApplySurfaceChangesTransactionState.preferredMinRefreshRate,
                     mTmpApplySurfaceChangesTransactionState.preferredMaxRefreshRate,
                     mTmpApplySurfaceChangesTransactionState.preferMinimalPostProcessing,
                     true /* inTraversal, must call performTraversalInTrans... below */);
@@ -4611,6 +4620,7 @@
         public boolean preferMinimalPostProcessing;
         public float preferredRefreshRate;
         public int preferredModeId;
+        public float preferredMinRefreshRate;
         public float preferredMaxRefreshRate;
 
         void reset() {
@@ -4620,6 +4630,7 @@
             preferMinimalPostProcessing = false;
             preferredRefreshRate = 0;
             preferredModeId = 0;
+            preferredMinRefreshRate = 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 deaf611..6bc42af 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -154,6 +154,21 @@
         return 0;
     }
 
+    float getPreferredMinRefreshRate(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;
+        }
+
+        return w.mAttrs.preferredMinDisplayRefreshRate;
+    }
+
     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.
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 cae6c86..a205a1d 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -900,10 +900,10 @@
     }
 
     @Test
-    public void testAppRequestMaxRefreshRate() {
-        // Confirm that the app max request range doesn't include flicker or min refresh rate
+    public void testAppRequestMinRefreshRate() {
+        // Confirm that the app min request range doesn't include flicker or min refresh rate
         // settings but does include everything else.
-        assertTrue(Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE
+        assertTrue(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE
                 >= Vote.APP_REQUEST_REFRESH_RATE_RANGE_PRIORITY_CUTOFF);
 
         Display.Mode[] modes = new Display.Mode[3];
@@ -936,7 +936,54 @@
         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));
+        votes.put(Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE,
+                Vote.forRefreshRates(75, Float.POSITIVE_INFINITY));
+        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);
+        assertThat(desiredSpecs.appRequestRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
+        assertThat(desiredSpecs.appRequestRefreshRateRange.max).isAtLeast(90f);
+    }
+
+    @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_REFRESH_RATE_RANGE
+                >= 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_REFRESH_RATE_RANGE, Vote.forRefreshRates(0, 75));
         director.injectVotesByDisplay(votesByDisplay);
         desiredSpecs = director.getDesiredDisplayModeSpecs(DISPLAY_ID);
         assertThat(desiredSpecs.primaryRefreshRateRange.min).isWithin(FLOAT_TOLERANCE).of(75);
@@ -948,7 +995,7 @@
     @Test
     public void testAppRequestObserver_modeId() {
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
-        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 0);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 0, 0);
 
         Vote appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
@@ -969,11 +1016,11 @@
         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);
+        Vote appRequestRefreshRateRange =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+        assertNull(appRequestRefreshRateRange);
 
-        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 90, 0);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 90, 0, 0);
 
         appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
@@ -992,15 +1039,15 @@
         assertThat(appRequestSize.height).isEqualTo(1000);
         assertThat(appRequestSize.width).isEqualTo(1000);
 
-        appRequestMaxRefreshRate =
-                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_MAX_REFRESH_RATE);
-        assertNull(appRequestMaxRefreshRate);
+        appRequestRefreshRateRange =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+        assertNull(appRequestRefreshRateRange);
     }
 
     @Test
-    public void testAppRequestObserver_maxRefreshRate() {
+    public void testAppRequestObserver_minRefreshRate() {
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
-        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 90);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 60, 0);
         Vote appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertNull(appRequestRefreshRate);
@@ -1008,15 +1055,16 @@
         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);
+        Vote appRequestRefreshRateRange =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+        assertNotNull(appRequestRefreshRateRange);
+        assertThat(appRequestRefreshRateRange.refreshRateRange.min)
+                .isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(appRequestRefreshRateRange.refreshRateRange.max).isAtLeast(90);
+        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
 
-        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 60);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 90, 0);
         appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
         assertNull(appRequestRefreshRate);
@@ -1024,19 +1072,74 @@
         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);
+        appRequestRefreshRateRange =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+        assertNotNull(appRequestRefreshRateRange);
+        assertThat(appRequestRefreshRateRange.refreshRateRange.min)
+                .isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(appRequestRefreshRateRange.refreshRateRange.max).isAtLeast(90);
+        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
     }
 
     @Test
-    public void testAppRequestObserver_modeIdAndMaxRefreshRate() {
+    public void testAppRequestObserver_maxRefreshRate() {
         DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
-        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 90);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 0, 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 appRequestRefreshRateRange =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+        assertNotNull(appRequestRefreshRateRange);
+        assertThat(appRequestRefreshRateRange.refreshRateRange.min).isZero();
+        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+                .isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 0, 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);
+
+        appRequestRefreshRateRange =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+        assertNotNull(appRequestRefreshRateRange);
+        assertThat(appRequestRefreshRateRange.refreshRateRange.min).isZero();
+        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+                .isWithin(FLOAT_TOLERANCE).of(60);
+        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
+    }
+
+    @Test
+    public void testAppRequestObserver_invalidRefreshRateRange() {
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, -1, 90, 60);
+        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 appRequestRefreshRateRange =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+        assertNull(appRequestRefreshRateRange);
+    }
+
+    @Test
+    public void testAppRequestObserver_modeIdAndRefreshRateRange() {
+        DisplayModeDirector director = createDirectorFromFpsRange(60, 90);
+        director.getAppRequestObserver().setAppRequest(DISPLAY_ID, 60, 90, 90);
 
         Vote appRequestRefreshRate =
                 director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_BASE_MODE_REFRESH_RATE);
@@ -1056,13 +1159,15 @@
         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);
+        Vote appRequestRefreshRateRange =
+                director.getVote(DISPLAY_ID, Vote.PRIORITY_APP_REQUEST_REFRESH_RATE_RANGE);
+        assertNotNull(appRequestRefreshRateRange);
+        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+                .isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(appRequestRefreshRateRange.refreshRateRange.max)
+                .isWithin(FLOAT_TOLERANCE).of(90);
+        assertThat(appRequestRefreshRateRange.height).isEqualTo(INVALID_SIZE);
+        assertThat(appRequestRefreshRateRange.width).isEqualTo(INVALID_SIZE);
     }
 
     @Test
@@ -1161,7 +1266,7 @@
         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_REFRESH_RATE_RANGE, 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));
@@ -1172,7 +1277,7 @@
         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_REFRESH_RATE_RANGE, 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));
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 e169692..b41872b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -84,10 +84,12 @@
         parcelLayoutParams(cameraUsingWindow);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.addNonHighRefreshRatePackage("com.android.test");
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(60, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         mPolicy.removeNonHighRefreshRatePackage("com.android.test");
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
@@ -127,6 +129,7 @@
         mPolicy.addNonHighRefreshRatePackage("com.android.test");
         assertEquals(LOW_MODE_ID, mPolicy.getPreferredModeId(overrideWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
     }
 
@@ -143,6 +146,7 @@
         mPolicy.addNonHighRefreshRatePackage("com.android.test");
         assertEquals(0, mPolicy.getPreferredModeId(overrideWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(overrideWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMinRefreshRate(overrideWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(overrideWindow), FLOAT_TOLERANCE);
     }
 
@@ -156,6 +160,7 @@
         mPolicy.addNonHighRefreshRatePackage("com.android.test");
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(60, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
 
         cameraUsingWindow.mActivityRecord.mSurfaceAnimator.startAnimation(
@@ -163,6 +168,7 @@
                 false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
         assertEquals(0, mPolicy.getPreferredModeId(cameraUsingWindow));
         assertEquals(0, mPolicy.getPreferredRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMinRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(cameraUsingWindow), FLOAT_TOLERANCE);
     }
 
@@ -173,6 +179,7 @@
         parcelLayoutParams(window);
         assertEquals(0, mPolicy.getPreferredModeId(window));
         assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
         assertEquals(60, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
 
         window.mActivityRecord.mSurfaceAnimator.startAnimation(
@@ -180,6 +187,36 @@
                 false /* hidden */, ANIMATION_TYPE_APP_TRANSITION);
         assertEquals(0, mPolicy.getPreferredModeId(window));
         assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
+    }
+
+    @Test
+    public void testAppMinRefreshRate() {
+        final WindowState window = createWindow(null, TYPE_BASE_APPLICATION, "window");
+        window.mAttrs.preferredMinDisplayRefreshRate = 60f;
+        parcelLayoutParams(window);
+        assertEquals(0, mPolicy.getPreferredModeId(window));
+        assertEquals(0, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertEquals(60, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
+        assertEquals(0, 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);
+    }
+
+    @Test
+    public void testAppPreferredRefreshRate() {
+        final WindowState window = createWindow(null, TYPE_BASE_APPLICATION, "window");
+        window.mAttrs.preferredRefreshRate = 60f;
+        parcelLayoutParams(window);
+        assertEquals(0, mPolicy.getPreferredModeId(window));
+        assertEquals(60, mPolicy.getPreferredRefreshRate(window), FLOAT_TOLERANCE);
+        assertEquals(0, mPolicy.getPreferredMinRefreshRate(window), FLOAT_TOLERANCE);
         assertEquals(0, mPolicy.getPreferredMaxRefreshRate(window), FLOAT_TOLERANCE);
     }
 }