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/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 {