Handle DISPLAY_EVENT_FRAME_RATE_OVERRIDE

When getting DISPLAY_EVENT_FRAME_RATE_OVERRIDE from SurfaceFlinger,
expose the overridden frame rate to the relevant application
if the current refresh rate allows that.

Bug: 169271059
Bug: 169271062
Bug: 170503758
Test: manual test using SF backdoor
adb shell service call SurfaceFlinger 1039 i32 <uid> f <refresh rate>

Change-Id: I6ae1a98e6ca13e9d3d095a5713a6b0ca99652256
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index b046d1d..7b4889f 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -358,7 +358,7 @@
                     // listener.
                     DisplayInfo display = getDisplayInfoLocked(displayId);
                     if (display != null) {
-                        float refreshRate = display.getMode().getRefreshRate();
+                        float refreshRate = display.getRefreshRate();
                         // Signal native callbacks if we ever set a refresh rate.
                         nSignalNativeCallbacks(refreshRate);
                     }
@@ -862,7 +862,7 @@
             if (display != null) {
                 // We need to tell AChoreographer instances the current refresh rate so that apps
                 // can get it for free once a callback first registers.
-                float refreshRate = display.getMode().getRefreshRate();
+                float refreshRate = display.getRefreshRate();
                 nSignalNativeCallbacks(refreshRate);
             }
         }
diff --git a/core/java/android/view/Choreographer.java b/core/java/android/view/Choreographer.java
index 3da3184..59299f6 100644
--- a/core/java/android/view/Choreographer.java
+++ b/core/java/android/view/Choreographer.java
@@ -276,7 +276,7 @@
     private static float getRefreshRate() {
         DisplayInfo di = DisplayManagerGlobal.getInstance().getDisplayInfo(
                 Display.DEFAULT_DISPLAY);
-        return di.getMode().getRefreshRate();
+        return di.getRefreshRate();
     }
 
     /**
@@ -944,7 +944,7 @@
         private VsyncEventData mLastVsyncEventData = new VsyncEventData();
 
         public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
-            super(looper, vsyncSource, CONFIG_CHANGED_EVENT_SUPPRESS);
+            super(looper, vsyncSource, 0);
         }
 
         // TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 3021aa6a..4c6c97f 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -456,6 +456,9 @@
     // TODO (b/114338689): Remove the flag and use WindowManager#REMOVE_CONTENT_MODE_DESTROY
     public static final int REMOVE_MODE_DESTROY_CONTENT = 1;
 
+    /** @hide */
+    public static final int DISPLAY_MODE_ID_FOR_FRAME_RATE_OVERRIDE = 0xFF;
+
     /**
      * Internal method to create a display.
      * The display created with this method will have a static {@link DisplayAdjustments} applied.
@@ -886,7 +889,7 @@
     public float getRefreshRate() {
         synchronized (this) {
             updateDisplayInfoLocked();
-            return mDisplayInfo.getMode().getRefreshRate();
+            return mDisplayInfo.getRefreshRate();
         }
     }
 
@@ -1509,6 +1512,16 @@
                     Float.floatToIntBits(mRefreshRate) == Float.floatToIntBits(refreshRate);
         }
 
+        /**
+         * Returns {@code true} if this mode equals to the other mode in all parameters except
+         * the refresh rate.
+         *
+         * @hide
+         */
+        public boolean equalsExceptRefreshRate(@Nullable Display.Mode other) {
+            return mWidth == other.mWidth && mHeight == other.mHeight;
+        }
+
         @Override
         public boolean equals(@Nullable Object other) {
             if (this == other) {
diff --git a/core/java/android/view/DisplayEventReceiver.java b/core/java/android/view/DisplayEventReceiver.java
index e8a4ed4..5d4a4e5 100644
--- a/core/java/android/view/DisplayEventReceiver.java
+++ b/core/java/android/view/DisplayEventReceiver.java
@@ -23,6 +23,8 @@
 import android.os.MessageQueue;
 import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import dalvik.annotation.optimization.FastNative;
 import dalvik.system.CloseGuard;
 
@@ -56,18 +58,18 @@
     public static final int VSYNC_SOURCE_SURFACE_FLINGER = 1;
 
     /**
-     * Specifies to suppress config changed events from being generated from Surface Flinger.
-     * <p>
-     * Needs to be kept in sync with frameworks/native/include/gui/ISurfaceComposer.h
-     */
-    public static final int CONFIG_CHANGED_EVENT_SUPPRESS = 0;
-
-    /**
      * Specifies to generate config changed events from Surface Flinger.
      * <p>
      * Needs to be kept in sync with frameworks/native/include/gui/ISurfaceComposer.h
      */
-    public static final int CONFIG_CHANGED_EVENT_DISPATCH = 1;
+    public static final int EVENT_REGISTRATION_CONFIG_CHANGED_FLAG = 0x1;
+
+    /**
+     * Specifies to generate frame rate override events from Surface Flinger.
+     * <p>
+     * Needs to be kept in sync with frameworks/native/include/gui/ISurfaceComposer.h
+     */
+    public static final int EVENT_REGISTRATION_FRAME_RATE_OVERRIDE_FLAG = 0x2;
 
     private static final String TAG = "DisplayEventReceiver";
 
@@ -81,7 +83,7 @@
     private MessageQueue mMessageQueue;
 
     private static native long nativeInit(WeakReference<DisplayEventReceiver> receiver,
-            MessageQueue messageQueue, int vsyncSource, int configChanged);
+            MessageQueue messageQueue, int vsyncSource, int eventRegistration);
     private static native void nativeDispose(long receiverPtr);
     @FastNative
     private static native void nativeScheduleVsync(long receiverPtr);
@@ -93,7 +95,7 @@
      */
     @UnsupportedAppUsage
     public DisplayEventReceiver(Looper looper) {
-        this(looper, VSYNC_SOURCE_APP, CONFIG_CHANGED_EVENT_SUPPRESS);
+        this(looper, VSYNC_SOURCE_APP, 0);
     }
 
     /**
@@ -101,17 +103,17 @@
      *
      * @param looper The looper to use when invoking callbacks.
      * @param vsyncSource The source of the vsync tick. Must be on of the VSYNC_SOURCE_* values.
-     * @param configChanged Whether to dispatch config changed events. Must be one of the
-     * CONFIG_CHANGED_EVENT_* values.
+     * @param eventRegistration Which events to dispatch. Must be a bitfield consist of the
+     * EVENT_REGISTRATION_*_FLAG values.
      */
-    public DisplayEventReceiver(Looper looper, int vsyncSource, int configChanged) {
+    public DisplayEventReceiver(Looper looper, int vsyncSource, int eventRegistration) {
         if (looper == null) {
             throw new IllegalArgumentException("looper must not be null");
         }
 
         mMessageQueue = looper.getQueue();
         mReceiverPtr = nativeInit(new WeakReference<DisplayEventReceiver>(this), mMessageQueue,
-                vsyncSource, configChanged);
+                vsyncSource, eventRegistration);
 
         mCloseGuard.open("dispose");
     }
@@ -206,6 +208,41 @@
     }
 
     /**
+     * Represents a mapping between a UID and an override frame rate
+     */
+    public static class FrameRateOverride {
+        // The application uid
+        public final int uid;
+
+        // The frame rate that this application runs at
+        public final float frameRateHz;
+
+
+        @VisibleForTesting
+        public FrameRateOverride(int uid, float frameRateHz) {
+            this.uid = uid;
+            this.frameRateHz = frameRateHz;
+        }
+
+        @Override
+        public String toString() {
+            return "{uid=" + uid + " frameRateHz=" + frameRateHz + "}";
+        }
+    }
+
+    /**
+     * Called when frame rate override event is received.
+     *
+     * @param timestampNanos The timestamp of the event, in the {@link System#nanoTime()}
+     * timebase.
+     * @param physicalDisplayId Stable display ID that uniquely describes a (display, port) pair.
+     * @param overrides The mappings from uid to frame rates
+     */
+    public void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
+            FrameRateOverride[] overrides) {
+    }
+
+    /**
      * Schedules a single vertical sync pulse to be delivered when the next
      * display frame begins.
      */
@@ -240,4 +277,11 @@
         onConfigChanged(timestampNanos, physicalDisplayId, configId);
     }
 
+    // Called from native code.
+    @SuppressWarnings("unused")
+    private void dispatchFrameRateOverrides(long timestampNanos, long physicalDisplayId,
+            FrameRateOverride[] overrides) {
+        onFrameRateOverridesChanged(timestampNanos, physicalDisplayId, overrides);
+    }
+
 }
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index fe9a1a7..0201605 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -261,6 +261,11 @@
     public String ownerPackageName;
 
     /**
+     * The refresh rate override for this app. 0 means no override.
+     */
+    public float refreshRateOverride;
+
+    /**
      * @hide
      * Get current remove mode of the display - what actions should be performed with the display's
      * content when it is removed.
@@ -332,7 +337,8 @@
                 && state == other.state
                 && ownerUid == other.ownerUid
                 && Objects.equals(ownerPackageName, other.ownerPackageName)
-                && removeMode == other.removeMode;
+                && removeMode == other.removeMode
+                && refreshRateOverride == other.refreshRateOverride;
     }
 
     @Override
@@ -376,6 +382,7 @@
         ownerUid = other.ownerUid;
         ownerPackageName = other.ownerPackageName;
         removeMode = other.removeMode;
+        refreshRateOverride = other.refreshRateOverride;
     }
 
     public void readFromParcel(Parcel source) {
@@ -421,6 +428,7 @@
         ownerPackageName = source.readString8();
         uniqueId = source.readString8();
         removeMode = source.readInt();
+        refreshRateOverride = source.readFloat();
     }
 
     @Override
@@ -465,6 +473,7 @@
         dest.writeString8(ownerPackageName);
         dest.writeString8(uniqueId);
         dest.writeInt(removeMode);
+        dest.writeFloat(refreshRateOverride);
     }
 
     @Override
@@ -472,6 +481,17 @@
         return 0;
     }
 
+    /**
+     * Returns the refresh rate the application would experience.
+     */
+    public float getRefreshRate() {
+        if (refreshRateOverride > 0) {
+            return refreshRateOverride;
+        }
+
+        return getMode().getRefreshRate();
+    }
+
     public Display.Mode getMode() {
         return findMode(modeId);
     }
@@ -675,6 +695,9 @@
         }
         sb.append(", removeMode ");
         sb.append(removeMode);
+        sb.append(", refreshRateOverride ");
+        sb.append(refreshRateOverride);
+
         sb.append("}");
         return sb.toString();
     }
diff --git a/core/jni/android_view_DisplayEventReceiver.cpp b/core/jni/android_view_DisplayEventReceiver.cpp
index e7e9c31..6337680 100644
--- a/core/jni/android_view_DisplayEventReceiver.cpp
+++ b/core/jni/android_view_DisplayEventReceiver.cpp
@@ -41,14 +41,21 @@
     jmethodID dispatchVsync;
     jmethodID dispatchHotplug;
     jmethodID dispatchConfigChanged;
+    jmethodID dispatchFrameRateOverrides;
+
+    struct {
+        jclass clazz;
+        jmethodID init;
+    } frameRateOverrideClassInfo;
+
 } gDisplayEventReceiverClassInfo;
 
 
 class NativeDisplayEventReceiver : public DisplayEventDispatcher {
 public:
-    NativeDisplayEventReceiver(JNIEnv* env,
-            jobject receiverWeak, const sp<MessageQueue>& messageQueue, jint vsyncSource,
-            jint configChanged);
+    NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
+                               const sp<MessageQueue>& messageQueue, jint vsyncSource,
+                               jint eventRegistration);
 
     void dispose();
 
@@ -64,16 +71,17 @@
     void dispatchHotplug(nsecs_t timestamp, PhysicalDisplayId displayId, bool connected) override;
     void dispatchConfigChanged(nsecs_t timestamp, PhysicalDisplayId displayId,
                                int32_t configId, nsecs_t vsyncPeriod) override;
+    void dispatchFrameRateOverrides(nsecs_t timestamp, PhysicalDisplayId displayId,
+                                    std::vector<FrameRateOverride> overrides) override;
     void dispatchNullEvent(nsecs_t timestamp, PhysicalDisplayId displayId) override {}
 };
 
-
-NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env,
-        jobject receiverWeak, const sp<MessageQueue>& messageQueue, jint vsyncSource,
-        jint configChanged) :
-        DisplayEventDispatcher(messageQueue->getLooper(),
-                static_cast<ISurfaceComposer::VsyncSource>(vsyncSource),
-                static_cast<ISurfaceComposer::ConfigChanged>(configChanged)),
+NativeDisplayEventReceiver::NativeDisplayEventReceiver(JNIEnv* env, jobject receiverWeak,
+                                                       const sp<MessageQueue>& messageQueue,
+                                                       jint vsyncSource, jint eventRegistration)
+      : DisplayEventDispatcher(messageQueue->getLooper(),
+                               static_cast<ISurfaceComposer::VsyncSource>(vsyncSource),
+                               static_cast<ISurfaceComposer::EventRegistration>(eventRegistration)),
         mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)),
         mMessageQueue(messageQueue) {
     ALOGV("receiver %p ~ Initializing display event receiver.", this);
@@ -137,16 +145,48 @@
   mMessageQueue->raiseAndClearException(env, "dispatchConfigChanged");
 }
 
-static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,
-        jobject messageQueueObj, jint vsyncSource, jint configChanged) {
+void NativeDisplayEventReceiver::dispatchFrameRateOverrides(
+        nsecs_t timestamp, PhysicalDisplayId displayId, std::vector<FrameRateOverride> overrides) {
+    JNIEnv* env = AndroidRuntime::getJNIEnv();
+
+    ScopedLocalRef<jobject> receiverObj(env, jniGetReferent(env, mReceiverWeakGlobal));
+    if (receiverObj.get()) {
+        ALOGV("receiver %p ~ Invoking FrameRateOverride handler.", this);
+        const auto frameRateOverrideClass =
+                gDisplayEventReceiverClassInfo.frameRateOverrideClassInfo.clazz;
+        const auto frameRateOverrideInit =
+                gDisplayEventReceiverClassInfo.frameRateOverrideClassInfo.init;
+        auto frameRateOverrideInitObject =
+                env->NewObject(frameRateOverrideClass, frameRateOverrideInit, 0, 0);
+        auto frameRateOverrideArray = env->NewObjectArray(overrides.size(), frameRateOverrideClass,
+                                                          frameRateOverrideInitObject);
+        for (size_t i = 0; i < overrides.size(); i++) {
+            auto FrameRateOverrideObject =
+                    env->NewObject(frameRateOverrideClass, frameRateOverrideInit, overrides[i].uid,
+                                   overrides[i].frameRateHz);
+            env->SetObjectArrayElement(frameRateOverrideArray, i, FrameRateOverrideObject);
+        }
+
+        env->CallVoidMethod(receiverObj.get(),
+                            gDisplayEventReceiverClassInfo.dispatchFrameRateOverrides, timestamp,
+                            displayId.value, frameRateOverrideArray);
+        ALOGV("receiver %p ~ Returned from FrameRateOverride handler.", this);
+    }
+
+    mMessageQueue->raiseAndClearException(env, "dispatchConfigChanged");
+}
+
+static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak, jobject messageQueueObj,
+                        jint vsyncSource, jint eventRegistration) {
     sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
     if (messageQueue == NULL) {
         jniThrowRuntimeException(env, "MessageQueue is not initialized.");
         return 0;
     }
 
-    sp<NativeDisplayEventReceiver> receiver = new NativeDisplayEventReceiver(env,
-            receiverWeak, messageQueue, vsyncSource, configChanged);
+    sp<NativeDisplayEventReceiver> receiver =
+            new NativeDisplayEventReceiver(env, receiverWeak, messageQueue, vsyncSource,
+                                           eventRegistration);
     status_t status = receiver->initialize();
     if (status) {
         String8 message;
@@ -205,6 +245,18 @@
             gDisplayEventReceiverClassInfo.clazz, "dispatchHotplug", "(JJZ)V");
     gDisplayEventReceiverClassInfo.dispatchConfigChanged = GetMethodIDOrDie(env,
            gDisplayEventReceiverClassInfo.clazz, "dispatchConfigChanged", "(JJI)V");
+    gDisplayEventReceiverClassInfo.dispatchFrameRateOverrides =
+            GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.clazz,
+                             "dispatchFrameRateOverrides",
+                             "(JJ[Landroid/view/DisplayEventReceiver$FrameRateOverride;)V");
+
+    jclass frameRateOverrideClazz =
+            FindClassOrDie(env, "android/view/DisplayEventReceiver$FrameRateOverride");
+    gDisplayEventReceiverClassInfo.frameRateOverrideClassInfo.clazz =
+            MakeGlobalRefOrDie(env, frameRateOverrideClazz);
+    gDisplayEventReceiverClassInfo.frameRateOverrideClassInfo.init =
+            GetMethodIDOrDie(env, gDisplayEventReceiverClassInfo.frameRateOverrideClassInfo.clazz,
+                             "<init>", "(IF)V");
 
     return res;
 }
diff --git a/graphics/java/android/graphics/HardwareRenderer.java b/graphics/java/android/graphics/HardwareRenderer.java
index a7f2739..cb4dd9e 100644
--- a/graphics/java/android/graphics/HardwareRenderer.java
+++ b/graphics/java/android/graphics/HardwareRenderer.java
@@ -1163,7 +1163,7 @@
             // heuristic we don't need to be always 100% correct.
             Mode activeMode = display.getMode();
             nInitDisplayInfo(activeMode.getPhysicalWidth(), activeMode.getPhysicalHeight(),
-                    activeMode.getRefreshRate(), maxRefreshRate,
+                    display.getRefreshRate(), maxRefreshRate,
                     wideColorDataspace.mNativeDataspace, display.getAppVsyncOffsetNanos(),
                     display.getPresentationDeadlineNanos());
 
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index fe6500e..468d825 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -22,6 +22,7 @@
 import android.view.Display;
 import android.view.DisplayAddress;
 import android.view.DisplayCutout;
+import android.view.DisplayEventReceiver;
 import android.view.Surface;
 
 import java.util.Arrays;
@@ -333,6 +334,9 @@
      */
     public String ownerPackageName;
 
+    public DisplayEventReceiver.FrameRateOverride[] frameRateOverrides =
+            new DisplayEventReceiver.FrameRateOverride[0];
+
     public void setAssumedDensityForExternalDisplay(int width, int height) {
         densityDpi = Math.min(width, height) * DisplayMetrics.DENSITY_XHIGH / 1080;
         // Technically, these values should be smaller than the apparent density
@@ -386,7 +390,8 @@
                 || !Objects.equals(address, other.address)
                 || !Objects.equals(deviceProductInfo, other.deviceProductInfo)
                 || ownerUid != other.ownerUid
-                || !Objects.equals(ownerPackageName, other.ownerPackageName)) {
+                || !Objects.equals(ownerPackageName, other.ownerPackageName)
+                || !Objects.equals(frameRateOverrides, other.frameRateOverrides)) {
             diff |= DIFF_OTHER;
         }
         return diff;
@@ -425,6 +430,7 @@
         state = other.state;
         ownerUid = other.ownerUid;
         ownerPackageName = other.ownerPackageName;
+        frameRateOverrides = other.frameRateOverrides;
     }
 
     // For debugging purposes
@@ -461,6 +467,10 @@
             sb.append(", owner ").append(ownerPackageName);
             sb.append(" (uid ").append(ownerUid).append(")");
         }
+        sb.append(", frameRateOverride ");
+        for (DisplayEventReceiver.FrameRateOverride frameRateOverride : frameRateOverrides) {
+            sb.append(frameRateOverride).append(" ");
+        }
         sb.append(flagsToString(flags));
         sb.append("}");
         return sb.toString();
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 004e481..3ccd208 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -37,6 +37,9 @@
 import android.annotation.Nullable;
 import android.annotation.UserIdInt;
 import android.app.AppOpsManager;
+import android.app.compat.CompatChanges;
+import android.compat.annotation.ChangeId;
+import android.compat.annotation.EnabledSince;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ParceledListSlice;
@@ -83,7 +86,9 @@
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.provider.Settings;
+import android.sysprop.DisplayProperties;
 import android.text.TextUtils;
+import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.IntArray;
 import android.util.Pair;
@@ -92,6 +97,7 @@
 import android.util.SparseIntArray;
 import android.util.Spline;
 import android.view.Display;
+import android.view.DisplayEventReceiver;
 import android.view.DisplayInfo;
 import android.view.IDisplayFoldListener;
 import android.view.Surface;
@@ -114,6 +120,7 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Optional;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.function.Consumer;
@@ -181,6 +188,7 @@
     private static final int MSG_REQUEST_TRAVERSAL = 4;
     private static final int MSG_UPDATE_VIEWPORT = 5;
     private static final int MSG_LOAD_BRIGHTNESS_CONFIGURATION = 6;
+    private static final int MSG_DELIVER_DISPLAY_EVENT_FRAME_RATE_OVERRIDE = 7;
 
     private final Context mContext;
     private final DisplayManagerHandler mHandler;
@@ -357,6 +365,30 @@
     // Received notifications of the display-fold action
     private DisplayFoldListener mDisplayFoldListener;
 
+    private final boolean mAllowNonNativeRefreshRateOverride;
+
+    private static final float THRESHOLD_FOR_REFRESH_RATES_DIVIDERS = 0.1f;
+
+    /**
+     * Applications use {@link android.view.Display#getRefreshRate} and
+     * {@link android.view.Display.Mode#getRefreshRate} to know what is the display refresh rate.
+     * Starting with Android S, the platform might throttle down applications frame rate to a
+     * divisor of the refresh rate if it is more preferable (for example if the application called
+     * to {@link android.view.Surface#setFrameRate}).
+     * Applications will experience {@link android.view.Choreographer#postFrameCallback} callbacks
+     * and backpressure at the throttled frame rate.
+     *
+     * {@link android.view.Display#getRefreshRate} will always return the application frame rate
+     * and not the physical display refresh rate to allow applications to do frame pacing correctly.
+     *
+     * {@link android.view.Display.Mode#getRefreshRate} will return the application frame rate if
+     * compiled to a previous release and starting with Android S it will return the physical
+     * display refresh rate.
+     */
+    @ChangeId
+    @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.S)
+    static final long DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE = 170503758L;
+
     public DisplayManagerService(Context context) {
         this(context, new Injector());
     }
@@ -389,6 +421,7 @@
         mCurrentUserId = UserHandle.USER_SYSTEM;
         ColorSpace[] colorSpaces = SurfaceControl.getCompositionColorSpaces();
         mWideColorSpace = colorSpaces[1];
+        mAllowNonNativeRefreshRateOverride = mInjector.getAllowNonNativeRefreshRateOverride();
 
         mSystemReady = false;
     }
@@ -677,11 +710,82 @@
                 Settings.Secure.MINIMAL_POST_PROCESSING_ALLOWED, 1, UserHandle.USER_CURRENT) != 0;
     }
 
+    private DisplayInfo getDisplayInfoForFrameRateOverride(DisplayEventReceiver.FrameRateOverride[]
+            frameRateOverrides, DisplayInfo info, int callingUid) {
+        float frameRateHz = 0;
+        for (DisplayEventReceiver.FrameRateOverride frameRateOverride : frameRateOverrides) {
+            if (frameRateOverride.uid == callingUid) {
+                frameRateHz = frameRateOverride.frameRateHz;
+                break;
+            }
+        }
+        if (frameRateHz == 0) {
+            return info;
+        }
+
+        // Override the refresh rate only if it is a divider of the current
+        // refresh rate. This calculation needs to be in sync with the native code
+        // in RefreshRateConfigs::getRefreshRateDividerForUid
+        Display.Mode currentMode = info.getMode();
+        float numPeriods = currentMode.getRefreshRate() / frameRateHz;
+        float numPeriodsRound = Math.round(numPeriods);
+        if (Math.abs(numPeriods - numPeriodsRound) > THRESHOLD_FOR_REFRESH_RATES_DIVIDERS) {
+            return info;
+        }
+        frameRateHz = currentMode.getRefreshRate() / numPeriodsRound;
+
+        DisplayInfo overriddenInfo = new DisplayInfo();
+        overriddenInfo.copyFrom(info);
+        for (Display.Mode mode : info.supportedModes) {
+            if (!mode.equalsExceptRefreshRate(currentMode)) {
+                continue;
+            }
+
+            if (mode.getRefreshRate() >= frameRateHz - THRESHOLD_FOR_REFRESH_RATES_DIVIDERS
+                    && mode.getRefreshRate()
+                    <= frameRateHz + THRESHOLD_FOR_REFRESH_RATES_DIVIDERS) {
+                if (DEBUG) {
+                    Slog.d(TAG, "found matching modeId " + mode.getModeId());
+                }
+                overriddenInfo.refreshRateOverride = mode.getRefreshRate();
+
+                if (!CompatChanges.isChangeEnabled(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE,
+                        callingUid)) {
+                    overriddenInfo.modeId = mode.getModeId();
+                }
+                return overriddenInfo;
+            }
+        }
+
+        if (mAllowNonNativeRefreshRateOverride) {
+            overriddenInfo.refreshRateOverride = frameRateHz;
+            if (!CompatChanges.isChangeEnabled(DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE,
+                    callingUid)) {
+                overriddenInfo.supportedModes = Arrays.copyOf(info.supportedModes,
+                        info.supportedModes.length + 1);
+                overriddenInfo.supportedModes[overriddenInfo.supportedModes.length - 1] =
+                        new Display.Mode(Display.DISPLAY_MODE_ID_FOR_FRAME_RATE_OVERRIDE,
+                                currentMode.getPhysicalWidth(), currentMode.getPhysicalHeight(),
+                                overriddenInfo.refreshRateOverride);
+                overriddenInfo.modeId =
+                        overriddenInfo.supportedModes[overriddenInfo.supportedModes.length - 1]
+                                .getModeId();
+            }
+            return overriddenInfo;
+        }
+
+
+
+        return info;
+    }
+
     private DisplayInfo getDisplayInfoInternal(int displayId, int callingUid) {
         synchronized (mSyncRoot) {
             LogicalDisplay display = mLogicalDisplayMapper.getLocked(displayId);
             if (display != null) {
-                DisplayInfo info = display.getDisplayInfoLocked();
+                DisplayInfo info =
+                        getDisplayInfoForFrameRateOverride(display.getFrameRateOverrides(),
+                                display.getDisplayInfoLocked(), callingUid);
                 if (info.hasAccess(callingUid)
                         || isUidPresentOnDisplayInternal(callingUid, displayId)) {
                     return info;
@@ -691,14 +795,15 @@
         }
     }
 
-    private void registerCallbackInternal(IDisplayManagerCallback callback, int callingPid) {
+    private void registerCallbackInternal(IDisplayManagerCallback callback, int callingPid,
+            int callingUid) {
         synchronized (mSyncRoot) {
             if (mCallbacks.get(callingPid) != null) {
                 throw new SecurityException("The calling process has already "
                         + "registered an IDisplayManagerCallback.");
             }
 
-            CallbackRecord record = new CallbackRecord(callingPid, callback);
+            CallbackRecord record = new CallbackRecord(callingPid, callingUid, callback);
             try {
                 IBinder binder = callback.asBinder();
                 binder.linkToDeath(record, 0);
@@ -1034,6 +1139,16 @@
         scheduleTraversalLocked(false);
     }
 
+    private void handleLogicalDisplayFrameRateOverridesChangedLocked(
+            @NonNull LogicalDisplay display) {
+        final int displayId = display.getDisplayIdLocked();
+        // We don't bother invalidating the display info caches here because any changes to the
+        // display info will trigger a cache invalidation inside of LogicalDisplay before we hit
+        // this point.
+        sendDisplayEventFrameRateOverrideLocked(displayId);
+        scheduleTraversalLocked(false);
+    }
+
     private void handleLogicalDisplayRemovedLocked(@NonNull LogicalDisplay display) {
         final int displayId = display.getDisplayIdLocked();
         mDisplayPowerControllers.delete(displayId);
@@ -1545,6 +1660,12 @@
         mHandler.sendMessage(msg);
     }
 
+    private void sendDisplayEventFrameRateOverrideLocked(int displayId) {
+        Message msg = mHandler.obtainMessage(MSG_DELIVER_DISPLAY_EVENT_FRAME_RATE_OVERRIDE,
+                displayId, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED);
+        mHandler.sendMessage(msg);
+    }
+
     // Requests that performTraversals be called at a
     // later time to apply changes to surfaces and displays.
     private void scheduleTraversalLocked(boolean inTraversal) {
@@ -1558,7 +1679,7 @@
 
     // Runs on Handler thread.
     // Delivers display event notifications to callbacks.
-    private void deliverDisplayEvent(int displayId, int event) {
+    private void deliverDisplayEvent(int displayId, ArraySet<Integer> uids, int event) {
         if (DEBUG) {
             Slog.d(TAG, "Delivering display event: displayId="
                     + displayId + ", event=" + event);
@@ -1570,12 +1691,14 @@
             count = mCallbacks.size();
             mTempCallbacks.clear();
             for (int i = 0; i < count; i++) {
-                mTempCallbacks.add(mCallbacks.valueAt(i));
+                if (uids == null || uids.contains(mCallbacks.valueAt(i).mUid)) {
+                    mTempCallbacks.add(mCallbacks.valueAt(i));
+                }
             }
         }
 
         // After releasing the lock, send the notifications out.
-        for (int i = 0; i < count; i++) {
+        for (int i = 0; i < mTempCallbacks.size(); i++) {
             mTempCallbacks.get(i).notifyDisplayEventAsync(displayId, event);
         }
         mTempCallbacks.clear();
@@ -1691,6 +1814,11 @@
         long getDefaultDisplayDelayTimeout() {
             return WAIT_FOR_DEFAULT_DISPLAY_TIMEOUT;
         }
+
+        boolean getAllowNonNativeRefreshRateOverride() {
+            return DisplayProperties
+                    .debug_allow_non_native_refresh_rate_override().orElse(false);
+        }
     }
 
     @VisibleForTesting
@@ -1760,7 +1888,7 @@
                     break;
 
                 case MSG_DELIVER_DISPLAY_EVENT:
-                    deliverDisplayEvent(msg.arg1, msg.arg2);
+                    deliverDisplayEvent(msg.arg1, null, msg.arg2);
                     break;
 
                 case MSG_REQUEST_TRAVERSAL:
@@ -1787,6 +1915,17 @@
                 case MSG_LOAD_BRIGHTNESS_CONFIGURATION:
                     loadBrightnessConfiguration();
                     break;
+
+                case MSG_DELIVER_DISPLAY_EVENT_FRAME_RATE_OVERRIDE:
+                    ArraySet<Integer> uids;
+                    synchronized (mSyncRoot) {
+                        int displayId = msg.arg1;
+                        LogicalDisplay display = mLogicalDisplayMapper.getLocked(displayId);
+                        uids = display.getPendingFrameRateOverrideUids();
+                        display.clearPendingFrameRateOverrideUids();
+                    }
+                    deliverDisplayEvent(msg.arg1, uids, msg.arg2);
+                    break;
             }
         }
     }
@@ -1810,6 +1949,10 @@
                 case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_SWAPPED:
                     handleLogicalDisplaySwappedLocked(display);
                     break;
+
+                case LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED:
+                    handleLogicalDisplayFrameRateOverridesChangedLocked(display);
+                    break;
             }
         }
 
@@ -1823,12 +1966,14 @@
 
     private final class CallbackRecord implements DeathRecipient {
         public final int mPid;
+        public final int mUid;
         private final IDisplayManagerCallback mCallback;
 
         public boolean mWifiDisplayScanRequested;
 
-        public CallbackRecord(int pid, IDisplayManagerCallback callback) {
+        CallbackRecord(int pid, int uid, IDisplayManagerCallback callback) {
             mPid = pid;
+            mUid = uid;
             mCallback = callback;
         }
 
@@ -1918,9 +2063,10 @@
             }
 
             final int callingPid = Binder.getCallingPid();
+            final int callingUid = Binder.getCallingUid();
             final long token = Binder.clearCallingIdentity();
             try {
-                registerCallbackInternal(callback, callingPid);
+                registerCallbackInternal(callback, callingPid, callingUid);
             } finally {
                 Binder.restoreCallingIdentity(token);
             }
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index f657858..74ea2d7 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -208,6 +208,9 @@
 
         private DisplayDeviceConfig mDisplayDeviceConfig;
 
+        private DisplayEventReceiver.FrameRateOverride[] mFrameRateOverrides =
+                new DisplayEventReceiver.FrameRateOverride[0];
+
         LocalDisplayDevice(IBinder displayToken, long physicalDisplayId,
                 SurfaceControl.DisplayInfo info, SurfaceControl.DisplayConfig[] configs,
                 int activeConfigId, SurfaceControl.DesiredDisplayConfigSpecs configSpecs,
@@ -625,6 +628,8 @@
                     mInfo.name = getContext().getResources().getString(
                             com.android.internal.R.string.display_manager_hdmi_display_name);
                 }
+                mInfo.frameRateOverrides = mFrameRateOverrides;
+
                 // The display is trusted since it is created by system.
                 mInfo.flags |= DisplayDeviceInfo.FLAG_TRUSTED;
             }
@@ -882,6 +887,13 @@
             }
         }
 
+        public void onFrameRateOverridesChanged(
+                DisplayEventReceiver.FrameRateOverride[] overrides) {
+            if (updateFrameRateOverridesLocked(overrides)) {
+                updateDeviceInfoLocked();
+            }
+        }
+
         public boolean updateActiveModeLocked(int activeConfigId) {
             if (mActiveConfigId == activeConfigId) {
                 return false;
@@ -895,6 +907,16 @@
             return true;
         }
 
+        public boolean updateFrameRateOverridesLocked(
+                DisplayEventReceiver.FrameRateOverride[] overrides) {
+            if (overrides.equals(mFrameRateOverrides)) {
+                return false;
+            }
+
+            mFrameRateOverrides = overrides;
+            return true;
+        }
+
         public void requestColorModeLocked(int colorMode) {
             if (mActiveColorMode == colorMode) {
                 return;
@@ -1102,23 +1124,39 @@
     public interface DisplayEventListener {
         void onHotplug(long timestampNanos, long physicalDisplayId, boolean connected);
         void onConfigChanged(long timestampNanos, long physicalDisplayId, int configId);
+        void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
+                DisplayEventReceiver.FrameRateOverride[] overrides);
+
     }
 
     public static final class ProxyDisplayEventReceiver extends DisplayEventReceiver {
         private final DisplayEventListener mListener;
         ProxyDisplayEventReceiver(Looper looper, DisplayEventListener listener) {
-            super(looper, VSYNC_SOURCE_APP, CONFIG_CHANGED_EVENT_DISPATCH);
+            super(looper, VSYNC_SOURCE_APP,
+                    EVENT_REGISTRATION_CONFIG_CHANGED_FLAG
+                            | EVENT_REGISTRATION_FRAME_RATE_OVERRIDE_FLAG);
             mListener = listener;
         }
+
+        @Override
         public void onHotplug(long timestampNanos, long physicalDisplayId, boolean connected) {
             mListener.onHotplug(timestampNanos, physicalDisplayId, connected);
         }
+
+        @Override
         public void onConfigChanged(long timestampNanos, long physicalDisplayId, int configId) {
             mListener.onConfigChanged(timestampNanos, physicalDisplayId, configId);
         }
+
+        @Override
+        public void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
+                DisplayEventReceiver.FrameRateOverride[] overrides) {
+            mListener.onFrameRateOverridesChanged(timestampNanos, physicalDisplayId, overrides);
+        }
     }
 
     private final class LocalDisplayEventListener implements DisplayEventListener {
+        @Override
         public void onHotplug(long timestampNanos, long physicalDisplayId, boolean connected) {
             synchronized (getSyncRoot()) {
                 if (connected) {
@@ -1128,6 +1166,8 @@
                 }
             }
         }
+
+        @Override
         public void onConfigChanged(long timestampNanos, long physicalDisplayId, int configId) {
             if (DEBUG) {
                 Slog.d(TAG, "onConfigChanged("
@@ -1147,5 +1187,26 @@
                 device.onActiveDisplayConfigChangedLocked(configId);
             }
         }
+
+        @Override
+        public void onFrameRateOverridesChanged(long timestampNanos, long physicalDisplayId,
+                DisplayEventReceiver.FrameRateOverride[] overrides) {
+            if (DEBUG) {
+                Slog.d(TAG, "onFrameRateOverrideChanged(timestampNanos=" + timestampNanos
+                        + ", physicalDisplayId=" + physicalDisplayId + " overrides="
+                        + Arrays.toString(overrides) + ")");
+            }
+            synchronized (getSyncRoot()) {
+                LocalDisplayDevice device = mDevices.get(physicalDisplayId);
+                if (device == null) {
+                    if (DEBUG) {
+                        Slog.d(TAG, "Received frame rate override event for unhandled physical"
+                                + " display: physicalDisplayId=" + physicalDisplayId);
+                    }
+                    return;
+                }
+                device.onFrameRateOverridesChanged(overrides);
+            }
+        }
     }
 }
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 979c3b8..d80e168 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -20,8 +20,11 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.display.DisplayManagerInternal;
+import android.util.ArraySet;
 import android.util.Slog;
+import android.util.SparseArray;
 import android.view.Display;
+import android.view.DisplayEventReceiver;
 import android.view.DisplayInfo;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -123,10 +126,27 @@
      */
     private boolean mIsEnabled = true;
 
+    /**
+     * The UID mappings for refresh rate override
+     */
+    private DisplayEventReceiver.FrameRateOverride[] mFrameRateOverrides;
+
+    /**
+     * Holds a set of UIDs that their frame rate override changed and needs to be notified
+     */
+    private ArraySet<Integer> mPendingFrameRateOverrideUids;
+
+    /**
+     * Temporary frame rate override list, used when needed.
+     */
+    private final SparseArray<Float> mTempFrameRateOverride;
+
     public LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice) {
         mDisplayId = displayId;
         mLayerStack = layerStack;
         mPrimaryDisplayDevice = primaryDisplayDevice;
+        mPendingFrameRateOverrideUids = new ArraySet<>();
+        mTempFrameRateOverride = new SparseArray<>();
     }
 
     /**
@@ -179,6 +199,27 @@
     }
 
     /**
+     * Returns the frame rate overrides list
+     */
+    public DisplayEventReceiver.FrameRateOverride[] getFrameRateOverrides() {
+        return mFrameRateOverrides;
+    }
+
+    /**
+     * Returns the list of uids that needs to be updated about their frame rate override
+     */
+    public ArraySet<Integer> getPendingFrameRateOverrideUids() {
+        return mPendingFrameRateOverrideUids;
+    }
+
+    /**
+     * Clears the list of uids that needs to be updated about their frame rate override
+     */
+    public void clearPendingFrameRateOverrideUids() {
+        mPendingFrameRateOverrideUids = new ArraySet<>();
+    }
+
+    /**
      * @see DisplayManagerInternal#getNonOverrideDisplayInfo(int, DisplayInfo)
      */
     void getNonOverrideDisplayInfoLocked(DisplayInfo outInfo) {
@@ -324,12 +365,40 @@
                     (deviceInfo.flags & DisplayDeviceInfo.FLAG_MASK_DISPLAY_CUTOUT) != 0;
             mBaseDisplayInfo.displayCutout = maskCutout ? null : deviceInfo.displayCutout;
             mBaseDisplayInfo.displayId = mDisplayId;
+            updateFrameRateOverrides(deviceInfo);
 
             mPrimaryDisplayDeviceInfo = deviceInfo;
             mInfo.set(null);
         }
     }
 
+    private void updateFrameRateOverrides(DisplayDeviceInfo deviceInfo) {
+        mTempFrameRateOverride.clear();
+        if (mFrameRateOverrides != null) {
+            for (DisplayEventReceiver.FrameRateOverride frameRateOverride
+                    : mFrameRateOverrides) {
+                mTempFrameRateOverride.put(frameRateOverride.uid,
+                        frameRateOverride.frameRateHz);
+            }
+        }
+        mFrameRateOverrides = deviceInfo.frameRateOverrides;
+        if (mFrameRateOverrides != null) {
+            for (DisplayEventReceiver.FrameRateOverride frameRateOverride
+                    : mFrameRateOverrides) {
+                float refreshRate = mTempFrameRateOverride.get(frameRateOverride.uid, 0f);
+                if (refreshRate == 0 || frameRateOverride.frameRateHz != refreshRate) {
+                    mTempFrameRateOverride.put(frameRateOverride.uid,
+                            frameRateOverride.frameRateHz);
+                } else {
+                    mTempFrameRateOverride.delete(frameRateOverride.uid);
+                }
+            }
+        }
+        for (int i = 0; i < mTempFrameRateOverride.size(); i++) {
+            mPendingFrameRateOverrideUids.add(mTempFrameRateOverride.keyAt(i));
+        }
+    }
+
     /**
      * Return the insets currently applied to the display.
      *
@@ -638,5 +707,7 @@
         pw.println("mBaseDisplayInfo=" + mBaseDisplayInfo);
         pw.println("mOverrideDisplayInfo=" + mOverrideDisplayInfo);
         pw.println("mRequestedMinimalPostProcessing=" + mRequestedMinimalPostProcessing);
+        pw.println("mFrameRateOverrides=" + Arrays.toString(mFrameRateOverrides));
+        pw.println("mPendingFrameRateOverrideUids=" + mPendingFrameRateOverrideUids);
     }
 }
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 45c38b4..ec8af78 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -23,6 +23,7 @@
 import android.util.Slog;
 import android.util.SparseArray;
 import android.view.Display;
+import android.view.DisplayEventReceiver;
 import android.view.DisplayInfo;
 
 import com.android.internal.util.IndentingPrintWriter;
@@ -52,6 +53,7 @@
     public static final int LOGICAL_DISPLAY_EVENT_CHANGED = 2;
     public static final int LOGICAL_DISPLAY_EVENT_REMOVED = 3;
     public static final int LOGICAL_DISPLAY_EVENT_SWAPPED = 4;
+    public static final int LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED = 5;
 
     /**
      * Temporary display info, used for comparing display configurations.
@@ -331,6 +333,8 @@
 
             mTempDisplayInfo.copyFrom(display.getDisplayInfoLocked());
             display.getNonOverrideDisplayInfoLocked(mTempNonOverrideDisplayInfo);
+            DisplayEventReceiver.FrameRateOverride[] frameRatesOverrides =
+                    display.getFrameRateOverrides();
             display.updateLocked(mDisplayDeviceRepo);
             if (!display.isValidLocked()) {
                 mLogicalDisplays.removeAt(i);
@@ -364,6 +368,9 @@
                 final int eventMsg = TextUtils.equals(oldUniqueId, newUniqueId)
                         ? LOGICAL_DISPLAY_EVENT_CHANGED : LOGICAL_DISPLAY_EVENT_SWAPPED;
                 mListener.onLogicalDisplayEventLocked(display, eventMsg);
+            } else if (!display.getPendingFrameRateOverrideUids().isEmpty()) {
+                mListener.onLogicalDisplayEventLocked(display,
+                        LogicalDisplayMapper.LOGICAL_DISPLAY_EVENT_FRAME_RATE_OVERRIDES_CHANGED);
             } else {
                 // While applications shouldn't know nor care about the non-overridden info, we
                 // still need to let WindowManager know so it can update its own internal state for
diff --git a/services/core/java/com/android/server/display/OverlayDisplayWindow.java b/services/core/java/com/android/server/display/OverlayDisplayWindow.java
index 49f0d35..cd3a453 100644
--- a/services/core/java/com/android/server/display/OverlayDisplayWindow.java
+++ b/services/core/java/com/android/server/display/OverlayDisplayWindow.java
@@ -16,8 +16,6 @@
 
 package com.android.server.display;
 
-import com.android.internal.util.DumpUtils;
-
 import android.content.Context;
 import android.graphics.SurfaceTexture;
 import android.hardware.display.DisplayManager;
@@ -30,12 +28,14 @@
 import android.view.MotionEvent;
 import android.view.ScaleGestureDetector;
 import android.view.TextureView;
+import android.view.TextureView.SurfaceTextureListener;
 import android.view.ThreadedRenderer;
 import android.view.View;
 import android.view.WindowManager;
-import android.view.TextureView.SurfaceTextureListener;
 import android.widget.TextView;
 
+import com.android.internal.util.DumpUtils;
+
 import java.io.PrintWriter;
 
 /**
@@ -319,7 +319,7 @@
         public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture,
                 int width, int height) {
             mListener.onWindowCreated(surfaceTexture,
-                    mDefaultDisplayInfo.getMode().getRefreshRate(),
+                    mDefaultDisplayInfo.getRefreshRate(),
                     mDefaultDisplayInfo.presentationDeadlineNanos, mDefaultDisplayInfo.state);
         }
 
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 1f72374..8ca057a 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -49,6 +49,8 @@
         // TODO: remove once Android migrates to JUnit 4.12,
         // which provides assertThrows
         "testng",
+        "junit",
+        "platform-compat-test-rules",
 
     ],
 
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
index 026db42..640d6e5 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -28,6 +28,7 @@
 import static org.mockito.Mockito.when;
 
 import android.app.PropertyInvalidatedCache;
+import android.compat.testing.PlatformCompatChangeRule;
 import android.content.Context;
 import android.graphics.Insets;
 import android.graphics.Rect;
@@ -44,8 +45,10 @@
 import android.hardware.input.InputManagerInternal;
 import android.os.Handler;
 import android.os.IBinder;
+import android.os.Process;
 import android.view.Display;
 import android.view.DisplayCutout;
+import android.view.DisplayEventReceiver;
 import android.view.DisplayInfo;
 import android.view.Surface;
 import android.view.SurfaceControl;
@@ -57,13 +60,17 @@
 
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
-import com.android.server.display.DisplayDeviceInfo;
 import com.android.server.display.DisplayManagerService.SyncRoot;
 import com.android.server.lights.LightsManager;
 import com.android.server.wm.WindowManagerInternal;
 
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
+
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
@@ -80,6 +87,9 @@
     private static final String VIRTUAL_DISPLAY_NAME = "Test Virtual Display";
     private static final String PACKAGE_NAME = "com.android.frameworks.servicestests";
 
+    @Rule
+    public TestRule compatChangeRule = new PlatformCompatChangeRule();
+
     private Context mContext;
 
     private final DisplayManagerService.Injector mShortMockedInjector =
@@ -95,15 +105,31 @@
                     return SHORT_DEFAULT_DISPLAY_TIMEOUT_MILLIS;
                 }
             };
-    private final DisplayManagerService.Injector mBasicInjector =
-            new DisplayManagerService.Injector() {
+
+    class BasicInjector extends DisplayManagerService.Injector {
+        @Override
+        VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot, Context context,
+                Handler handler, DisplayAdapter.Listener displayAdapterListener) {
+            return new VirtualDisplayAdapter(syncRoot, context, handler, displayAdapterListener,
+                    (String name, boolean secure) -> mMockDisplayToken);
+        }
+    }
+
+    private final DisplayManagerService.Injector mBasicInjector = new BasicInjector();
+
+    private final DisplayManagerService.Injector mAllowNonNativeRefreshRateOverrideInjector =
+            new BasicInjector() {
                 @Override
-                VirtualDisplayAdapter getVirtualDisplayAdapter(SyncRoot syncRoot,
-                        Context context, Handler handler,
-                        DisplayAdapter.Listener displayAdapterListener) {
-                    return new VirtualDisplayAdapter(syncRoot, context, handler,
-                            displayAdapterListener,
-                            (String name, boolean secure) -> mMockDisplayToken);
+                boolean getAllowNonNativeRefreshRateOverride() {
+                    return true;
+                }
+            };
+
+    private final DisplayManagerService.Injector mDenyNonNativeRefreshRateOverrideInjector =
+            new BasicInjector() {
+                @Override
+                boolean getAllowNonNativeRefreshRateOverride() {
+                    return false;
                 }
             };
 
@@ -575,6 +601,337 @@
         assertEquals(displayManager.getVirtualDisplaySurfaceInternal(mMockAppToken), surface);
     }
 
+    /**
+     * Tests that there should be a display change notification if the frame rate overrides
+     * list is updated.
+     */
+    @Test
+    public void testShouldNotifyChangeWhenDisplayInfoFrameRateOverrideChanged() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager, new float[]{60f});
+        FakeDisplayManagerCallback callback = registerDisplayListenerCallback(displayManager,
+                displayManagerBinderService, displayDevice);
+
+        int myUid = Process.myUid();
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(myUid, 30f),
+                });
+        assertTrue(callback.mCalled);
+        callback.clear();
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(myUid, 30f),
+                        new DisplayEventReceiver.FrameRateOverride(1234, 30f),
+                });
+        assertFalse(callback.mCalled);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(myUid, 20f),
+                        new DisplayEventReceiver.FrameRateOverride(1234, 30f),
+                        new DisplayEventReceiver.FrameRateOverride(5678, 30f),
+                });
+        assertTrue(callback.mCalled);
+        callback.clear();
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(1234, 30f),
+                        new DisplayEventReceiver.FrameRateOverride(5678, 30f),
+                });
+        assertTrue(callback.mCalled);
+        callback.clear();
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(5678, 30f),
+                });
+        assertFalse(callback.mCalled);
+    }
+
+    /**
+     * Tests that the DisplayInfo is updated correctly with a frame rate override
+     */
+    @Test
+    public void testDisplayInfoFrameRateOverride() throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f, 30f, 20f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid(), 20f),
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid() + 1, 30f)
+                });
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+
+        // Changing the mode to 30Hz should not override the refresh rate to 20Hz anymore
+        // as 20 is not a divider of 30.
+        updateModeId(displayManager, displayDevice, 2);
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(30f, displayInfo.getRefreshRate(), 0.01f);
+    }
+
+    /**
+     * Tests that the frame rate override is updated accordingly to the
+     * allowNonNativeRefreshRateOverride policy.
+     */
+    @Test
+    public void testDisplayInfoNonNativeFrameRateOverride() throws Exception {
+        testDisplayInfoNonNativeFrameRateOverride(mDenyNonNativeRefreshRateOverrideInjector);
+        testDisplayInfoNonNativeFrameRateOverride(mAllowNonNativeRefreshRateOverrideInjector);
+    }
+
+    /**
+     * Tests that the mode reflects the frame rate override is in compat mode
+     */
+    @Test
+    @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public  void testDisplayInfoFrameRateOverrideModeCompat() throws Exception {
+        testDisplayInfoFrameRateOverrideModeCompat(/*compatChangeEnabled*/ false);
+    }
+
+    /**
+     * Tests that the mode reflects the physical display refresh rate when not in compat mode.
+     */
+    @Test
+    @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public  void testDisplayInfoFrameRateOverrideMode() throws Exception {
+        testDisplayInfoFrameRateOverrideModeCompat(/*compatChangeEnabled*/ true);
+    }
+
+    /**
+     * Tests that the mode reflects the frame rate override is in compat mode and accordingly to the
+     * allowNonNativeRefreshRateOverride policy.
+     */
+    @Test
+    @DisableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public void testDisplayInfoNonNativeFrameRateOverrideModeCompat() throws Exception {
+        testDisplayInfoNonNativeFrameRateOverrideMode(mDenyNonNativeRefreshRateOverrideInjector,
+                /*compatChangeEnabled*/ false);
+        testDisplayInfoNonNativeFrameRateOverrideMode(mAllowNonNativeRefreshRateOverrideInjector,
+                /*compatChangeEnabled*/  false);
+    }
+
+    /**
+     * Tests that the mode reflects the physical display refresh rate when not in compat mode.
+     */
+    @Test
+    @EnableCompatChanges({DisplayManagerService.DISPLAY_MODE_RETURNS_PHYSICAL_REFRESH_RATE})
+    public void testDisplayInfoNonNativeFrameRateOverrideMode() throws Exception {
+        testDisplayInfoNonNativeFrameRateOverrideMode(mDenyNonNativeRefreshRateOverrideInjector,
+                /*compatChangeEnabled*/  true);
+        testDisplayInfoNonNativeFrameRateOverrideMode(mAllowNonNativeRefreshRateOverrideInjector,
+                /*compatChangeEnabled*/  true);
+    }
+
+    private void testDisplayInfoFrameRateOverrideModeCompat(boolean compatChangeEnabled)
+            throws Exception {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, mShortMockedInjector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f, 30f, 20f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid(), 20f),
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid() + 1, 30f)
+                });
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(20f, displayInfo.getRefreshRate(), 0.01f);
+        Display.Mode expectedMode;
+        if (compatChangeEnabled) {
+            expectedMode = new Display.Mode(1, 100, 200, 60f);
+        } else {
+            expectedMode = new Display.Mode(3, 100, 200, 20f);
+        }
+        assertEquals(expectedMode, displayInfo.getMode());
+    }
+
+    private void testDisplayInfoNonNativeFrameRateOverrideMode(
+            DisplayManagerService.Injector injector, boolean compatChangeEnabled) {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, injector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid(), 20f)
+                });
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        Display.Mode expectedMode;
+        if (compatChangeEnabled) {
+            expectedMode = new Display.Mode(1, 100, 200, 60f);
+        } else if (injector.getAllowNonNativeRefreshRateOverride()) {
+            expectedMode = new Display.Mode(255, 100, 200, 20f);
+        } else {
+            expectedMode = new Display.Mode(1, 100, 200, 60f);
+        }
+        assertEquals(expectedMode, displayInfo.getMode());
+    }
+
+    private void testDisplayInfoNonNativeFrameRateOverride(
+            DisplayManagerService.Injector injector) {
+        DisplayManagerService displayManager =
+                new DisplayManagerService(mContext, injector);
+        DisplayManagerService.BinderService displayManagerBinderService =
+                displayManager.new BinderService();
+        registerDefaultDisplays(displayManager);
+        displayManager.onBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);
+
+        FakeDisplayDevice displayDevice = createFakeDisplayDevice(displayManager,
+                new float[]{60f});
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+        DisplayInfo displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        assertEquals(60f, displayInfo.getRefreshRate(), 0.01f);
+
+        updateFrameRateOverride(displayManager, displayDevice,
+                new DisplayEventReceiver.FrameRateOverride[]{
+                        new DisplayEventReceiver.FrameRateOverride(
+                                Process.myUid(), 20f)
+                });
+        displayInfo = displayManagerBinderService.getDisplayInfo(displayId);
+        float expectedRefreshRate = injector.getAllowNonNativeRefreshRateOverride() ? 20f : 60f;
+        assertEquals(expectedRefreshRate, displayInfo.getRefreshRate(), 0.01f);
+    }
+
+    private int getDisplayIdForDisplayDevice(
+            DisplayManagerService displayManager,
+            DisplayManagerService.BinderService displayManagerBinderService,
+            FakeDisplayDevice displayDevice) {
+
+        final int[] displayIds = displayManagerBinderService.getDisplayIds();
+        assertTrue(displayIds.length > 0);
+        int displayId = Display.INVALID_DISPLAY;
+        for (int i = 0; i < displayIds.length; i++) {
+            DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayIds[i]);
+            if (displayDevice.getDisplayDeviceInfoLocked().equals(ddi)) {
+                displayId = displayIds[i];
+                break;
+            }
+        }
+        assertFalse(displayId == Display.INVALID_DISPLAY);
+        return displayId;
+    }
+
+    private void updateDisplayDeviceInfo(DisplayManagerService displayManager,
+            FakeDisplayDevice displayDevice,
+            DisplayDeviceInfo displayDeviceInfo) {
+        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_CHANGED);
+        Handler handler = displayManager.getDisplayHandler();
+        handler.runWithScissors(() -> {
+        }, 0 /* now */);
+    }
+
+    private void updateFrameRateOverride(DisplayManagerService displayManager,
+            FakeDisplayDevice displayDevice,
+            DisplayEventReceiver.FrameRateOverride[] frameRateOverrides) {
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
+        displayDeviceInfo.frameRateOverrides = frameRateOverrides;
+        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
+    }
+
+    private void updateModeId(DisplayManagerService displayManager,
+            FakeDisplayDevice displayDevice,
+            int modeId) {
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        displayDeviceInfo.copyFrom(displayDevice.getDisplayDeviceInfoLocked());
+        displayDeviceInfo.modeId = modeId;
+        updateDisplayDeviceInfo(displayManager, displayDevice, displayDeviceInfo);
+    }
+
+    private FakeDisplayManagerCallback registerDisplayListenerCallback(
+            DisplayManagerService displayManager,
+            DisplayManagerService.BinderService displayManagerBinderService,
+            FakeDisplayDevice displayDevice) {
+        // Find the display id of the added FakeDisplayDevice
+        DisplayDeviceInfo displayDeviceInfo = displayDevice.getDisplayDeviceInfoLocked();
+
+        int displayId = getDisplayIdForDisplayDevice(displayManager, displayManagerBinderService,
+                displayDevice);
+
+        Handler handler = displayManager.getDisplayHandler();
+        handler.runWithScissors(() -> {
+        }, 0 /* now */);
+
+        // register display listener callback
+        FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(displayId);
+        displayManagerBinderService.registerCallback(callback);
+        return callback;
+    }
+
+    private FakeDisplayDevice createFakeDisplayDevice(DisplayManagerService displayManager,
+            float[] refreshRates) {
+        FakeDisplayDevice displayDevice = new FakeDisplayDevice();
+        DisplayDeviceInfo displayDeviceInfo = new DisplayDeviceInfo();
+        int width = 100;
+        int height = 200;
+        displayDeviceInfo.supportedModes = new Display.Mode[refreshRates.length];
+        for (int i = 0; i < refreshRates.length; i++) {
+            displayDeviceInfo.supportedModes[i] =
+                    new Display.Mode(i + 1, width, height, refreshRates[i]);
+        }
+        displayDeviceInfo.modeId = 1;
+        displayDeviceInfo.width = width;
+        displayDeviceInfo.height = height;
+        final Rect zeroRect = new Rect();
+        displayDeviceInfo.displayCutout = new DisplayCutout(
+                Insets.of(0, 10, 0, 0),
+                zeroRect, new Rect(0, 0, 10, 10), zeroRect, zeroRect);
+        displayDeviceInfo.flags = DisplayDeviceInfo.FLAG_DEFAULT_DISPLAY;
+        displayDevice.setDisplayDeviceInfo(displayDeviceInfo);
+        displayManager.getDisplayDeviceRepository()
+                .onDisplayDeviceEvent(displayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
+        return displayDevice;
+    }
+
     private void registerDefaultDisplays(DisplayManagerService displayManager) {
         Handler handler = displayManager.getDisplayHandler();
         // Would prefer to call displayManager.onStart() directly here but it performs binderService
@@ -598,6 +955,10 @@
                 mCalled = true;
             }
         }
+
+        public void clear() {
+            mCalled = false;
+        }
     }
 
     private class FakeDisplayDevice extends DisplayDevice {