Default resolution settings [Backend]

Added new APIs to DisplayManager to:
1. set the user preferred display mode,
2. clear user preferred display mode and
3. wheteher user has chosen a display mode

These new settings are stored in Settings.Global.

Bug: 192332479
Test: atest DefaultDisplayModeTest
Test: atest LocalDisplayAdapterTest
Change-Id: I3a60e271c7ec23c84e74487e89b3e29ca91e8a40
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index b8ec6fc..bbdf8e8 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -1179,12 +1179,15 @@
 
   public final class DisplayManager {
     method public boolean areUserDisabledHdrTypesAllowed();
+    method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void clearUserPreferredDisplayMode();
     method @NonNull public int[] getUserDisabledHdrTypes();
+    method @Nullable public android.view.Display.Mode getUserPreferredDisplayMode();
     method public boolean isMinimalPostProcessingRequested(int);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setAreUserDisabledHdrTypesAllowed(boolean);
     method @RequiresPermission(android.Manifest.permission.MODIFY_REFRESH_RATE_SWITCHING_TYPE) public void setRefreshRateSwitchingType(int);
     method @RequiresPermission(android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS) public void setShouldAlwaysRespectAppRequestedMode(boolean);
     method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setUserDisabledHdrTypes(@NonNull int[]);
+    method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void setUserPreferredDisplayMode(@NonNull android.view.Display.Mode);
     method @RequiresPermission(android.Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS) public boolean shouldAlwaysRespectAppRequestedMode();
     field public static final int SWITCHING_TYPE_ACROSS_AND_WITHIN_GROUPS = 2; // 0x2
     field public static final int SWITCHING_TYPE_NONE = 0; // 0x0
@@ -2167,6 +2170,9 @@
     field public static final String OVERLAY_DISPLAY_DEVICES = "overlay_display_devices";
     field public static final String SHOW_FIRST_CRASH_DIALOG = "show_first_crash_dialog";
     field public static final String USER_DISABLED_HDR_FORMATS = "user_disabled_hdr_formats";
+    field public static final String USER_PREFERRED_REFRESH_RATE = "user_preferred_refresh_rate";
+    field public static final String USER_PREFERRED_RESOLUTION_HEIGHT = "user_preferred_resolution_height";
+    field public static final String USER_PREFERRED_RESOLUTION_WIDTH = "user_preferred_resolution_width";
     field public static final String USE_OPEN_WIFI_PACKAGE = "use_open_wifi_package";
   }
 
@@ -2734,6 +2740,7 @@
   }
 
   public final class Display {
+    method @NonNull public android.view.Display.Mode getDefaultMode();
     method @NonNull public int[] getReportedHdrTypes();
     method @NonNull public android.graphics.ColorSpace[] getSupportedWideColorGamut();
     method public int getType();
@@ -2747,6 +2754,11 @@
     field public static final int TYPE_WIFI = 3; // 0x3
   }
 
+  public static final class Display.Mode implements android.os.Parcelable {
+    ctor public Display.Mode(int, int, float);
+    method public boolean matches(int, int, float);
+  }
+
   public class FocusFinder {
     method public static void sort(android.view.View[], int, int, android.view.ViewGroup, boolean);
   }
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index 73961ff..209ee40e 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -1113,6 +1113,44 @@
     }
 
     /**
+     * Sets the default display mode, according to the refresh rate and the resolution chosen by the
+     * user.
+     *
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    public void setUserPreferredDisplayMode(@NonNull Display.Mode mode) {
+        // Create a new object containing default values for the unused fields like mode ID and
+        // alternative refresh rates.
+        Display.Mode preferredMode = new Display.Mode(mode.getPhysicalWidth(),
+                mode.getPhysicalHeight(), mode.getRefreshRate());
+        mGlobal.setUserPreferredDisplayMode(preferredMode);
+    }
+
+    /**
+     * Removes the user preferred display mode.
+     *
+     * @hide
+     */
+    @TestApi
+    @RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
+    public void clearUserPreferredDisplayMode() {
+        mGlobal.setUserPreferredDisplayMode(null);
+    }
+
+    /**
+     * Returns the user preferred display mode.
+     *
+     * @hide
+     */
+    @TestApi
+    @Nullable
+    public Display.Mode getUserPreferredDisplayMode() {
+        return mGlobal.getUserPreferredDisplayMode();
+    }
+
+    /**
      * When enabled the app requested mode is always selected regardless of user settings and
      * policies for low brightness, low battery, etc.
      *
diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java
index 1fec3c9f..75155bb 100644
--- a/core/java/android/hardware/display/DisplayManagerGlobal.java
+++ b/core/java/android/hardware/display/DisplayManagerGlobal.java
@@ -877,6 +877,29 @@
     }
 
     /**
+     * Sets the default display mode, according to the refresh rate and the resolution chosen by the
+     * user.
+     */
+    public void setUserPreferredDisplayMode(Display.Mode mode) {
+        try {
+            mDm.setUserPreferredDisplayMode(mode);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the user preferred display mode.
+     */
+    public Display.Mode getUserPreferredDisplayMode() {
+        try {
+            return mDm.getUserPreferredDisplayMode();
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * When enabled the app requested display resolution and refresh rate is always selected
      * in DisplayModeDirector regardless of user settings and policies for low brightness, low
      * battery etc.
diff --git a/core/java/android/hardware/display/IDisplayManager.aidl b/core/java/android/hardware/display/IDisplayManager.aidl
index 1162146..b3be9da 100644
--- a/core/java/android/hardware/display/IDisplayManager.aidl
+++ b/core/java/android/hardware/display/IDisplayManager.aidl
@@ -27,6 +27,7 @@
 import android.hardware.display.WifiDisplay;
 import android.hardware.display.WifiDisplayStatus;
 import android.media.projection.IMediaProjection;
+import android.view.Display.Mode;
 import android.view.DisplayInfo;
 import android.view.Surface;
 
@@ -162,6 +163,11 @@
     // based on hardware capability.
     int getPreferredWideGamutColorSpaceId();
 
+    // Sets the user preferred display mode.
+    // Requires WRITE_SECURE_SETTINGS permission.
+    void setUserPreferredDisplayMode(in Mode mode);
+    Mode getUserPreferredDisplayMode();
+
     // When enabled the app requested display resolution and refresh rate is always selected
     // in DisplayModeDirector regardless of user settings and policies for low brightness, low
     // battery etc.
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index a5b76d3..3c3b237 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -14602,6 +14602,38 @@
         public static final int HEADS_UP_ON = 1;
 
         /**
+         * The refresh rate chosen by the user.
+         *
+         * @hide
+         */
+        @TestApi
+        @Readable
+        @SuppressLint("NoSettingsProvider")
+        public static final String USER_PREFERRED_REFRESH_RATE = "user_preferred_refresh_rate";
+
+        /**
+         * The resolution height chosen by the user.
+         *
+         * @hide
+         */
+        @TestApi
+        @Readable
+        @SuppressLint("NoSettingsProvider")
+        public static final String USER_PREFERRED_RESOLUTION_HEIGHT =
+                "user_preferred_resolution_height";
+
+        /**
+         * The resolution width chosen by the user.
+         *
+         * @hide
+         */
+        @TestApi
+        @Readable
+        @SuppressLint("NoSettingsProvider")
+        public static final String USER_PREFERRED_RESOLUTION_WIDTH =
+                "user_preferred_resolution_width";
+
+        /**
          * The name of the device
          */
         @Readable
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index e7ff978..8259a9d 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -1000,6 +1000,18 @@
     }
 
     /**
+     * Returns the default mode of the display.
+     * @hide
+     */
+    @TestApi
+    public @NonNull Mode getDefaultMode() {
+        synchronized (mLock) {
+            updateDisplayInfoLocked();
+            return mDisplayInfo.getDefaultMode();
+        }
+    }
+
+    /**
      * Gets the supported modes of this display.
      */
     public Mode[] getSupportedModes() {
@@ -1699,6 +1711,11 @@
          */
         public static final Mode[] EMPTY_ARRAY = new Mode[0];
 
+        /**
+         * @hide
+         */
+        public static final int INVALID_MODE_ID = -1;
+
         private final int mModeId;
         private final int mWidth;
         private final int mHeight;
@@ -1709,6 +1726,14 @@
         /**
          * @hide
          */
+        @TestApi
+        public Mode(int width, int height, float refreshRate) {
+            this(INVALID_MODE_ID, width, height, refreshRate, new float[0]);
+        }
+
+        /**
+         * @hide
+         */
         @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
         public Mode(int modeId, int width, int height, float refreshRate) {
             this(modeId, width, height, refreshRate, new float[0]);
@@ -1804,6 +1829,7 @@
          *
          * @hide
          */
+        @TestApi
         public boolean matches(int width, int height, float refreshRate) {
             return mWidth == width &&
                     mHeight == height &&
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index 9972eba..9e5d122 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -1820,11 +1820,6 @@
      * @hide
      */
     public static final class DisplayMode {
-        /**
-         * Invalid display config id.
-         */
-        public static final int INVALID_DISPLAY_MODE_ID = -1;
-
         public int id;
         public int width;
         public int height;
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
index 80b7e10..6f42d59 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/GlobalSettings.java
@@ -81,6 +81,9 @@
         Settings.Global.POWER_BUTTON_LONG_PRESS,
         Settings.Global.AUTOMATIC_POWER_SAVE_MODE,
         Settings.Global.ADVANCED_BATTERY_USAGE_AMOUNT,
-        Settings.Global.POWER_BUTTON_LONG_PRESS_DURATION_MS
+        Settings.Global.POWER_BUTTON_LONG_PRESS_DURATION_MS,
+        Settings.Global.USER_PREFERRED_REFRESH_RATE,
+        Settings.Global.USER_PREFERRED_RESOLUTION_HEIGHT,
+        Settings.Global.USER_PREFERRED_RESOLUTION_WIDTH,
     };
 }
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 0a75eb8..93f900d 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -21,6 +21,7 @@
 import static android.provider.settings.validators.SettingsValidators.ANY_STRING_VALIDATOR;
 import static android.provider.settings.validators.SettingsValidators.BOOLEAN_VALIDATOR;
 import static android.provider.settings.validators.SettingsValidators.NONE_NEGATIVE_LONG_VALIDATOR;
+import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_FLOAT_VALIDATOR;
 import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_INTEGER_VALIDATOR;
 import static android.provider.settings.validators.SettingsValidators.PACKAGE_NAME_VALIDATOR;
 import static android.provider.settings.validators.SettingsValidators.PERCENTAGE_INTEGER_VALIDATOR;
@@ -321,6 +322,9 @@
         VALIDATORS.put(Global.Wearable.COMBINED_LOCATION_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Global.Wearable.WRIST_ORIENTATION_MODE,
                        new DiscreteValueValidator(new String[] {"0", "1", "2", "3"}));
+        VALIDATORS.put(Global.USER_PREFERRED_REFRESH_RATE, NON_NEGATIVE_FLOAT_VALIDATOR);
+        VALIDATORS.put(Global.USER_PREFERRED_RESOLUTION_HEIGHT, ANY_INTEGER_VALIDATOR);
+        VALIDATORS.put(Global.USER_PREFERRED_RESOLUTION_WIDTH, ANY_INTEGER_VALIDATOR);
     }
 }
 
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java
index 223cc51a..49012b0 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java
@@ -74,6 +74,20 @@
         }
     };
 
+    public static final Validator NON_NEGATIVE_FLOAT_VALIDATOR = new Validator() {
+        @Override
+        public boolean validate(@Nullable String value) {
+            if (value == null) {
+                return true;
+            }
+            try {
+                return Float.parseFloat(value) >= 0.0f;
+            } catch (NumberFormatException e) {
+                return false;
+            }
+        }
+    };
+
     public static final Validator URI_VALIDATOR = new Validator() {
         @Override
         public boolean validate(@Nullable String value) {
diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java
index 806bcc2..def9685 100644
--- a/services/core/java/com/android/server/display/DisplayDevice.java
+++ b/services/core/java/com/android/server/display/DisplayDevice.java
@@ -206,6 +206,13 @@
             DisplayModeDirector.DesiredDisplayModeSpecs displayModeSpecs) {}
 
     /**
+     * Sets the user preferred display mode. Removes the user preferred display mode and sets
+     * default display mode as the mode chosen by HAL, if 'mode' is null
+     * Returns true if the mode set by user is supported by the display.
+     */
+    public void setUserPreferredDisplayModeLocked(Display.Mode mode) { }
+
+    /**
      * Sets the requested color mode.
      */
     public void setRequestedColorModeLocked(int colorMode) {
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 73bcea6..c964e37 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -130,6 +130,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.atomic.AtomicLong;
@@ -213,6 +214,9 @@
     private int[] mUserDisabledHdrTypes = {};
     private boolean mAreUserDisabledHdrTypesAllowed = true;
 
+    // Display mode chosen by user.
+    private Display.Mode mUserPreferredMode;
+
     // The synchronization root for the display manager.
     // This lock guards most of the display manager's state.
     // NOTE: This is synchronized on while holding WindowManagerService.mWindowMap so never call
@@ -581,6 +585,7 @@
             updateSettingsLocked();
 
             updateUserDisabledHdrTypesFromSettingsLocked();
+            updateUserPreferredDisplayModeSettingsLocked();
         }
 
         mDisplayModeDirector.setDesiredDisplayModeSpecsListener(
@@ -795,9 +800,8 @@
                     mUserDisabledHdrTypes[i] = Integer.parseInt(userDisabledHdrTypeStrings[i]);
                 }
             } catch (NumberFormatException e) {
-                Slog.e(TAG,
-                        "Failed to parse USER_DISABLED_HDR_FORMATS. "
-                                + "Clearing the setting.", e);
+                Slog.e(TAG, "Failed to parse USER_DISABLED_HDR_FORMATS. "
+                        + "Clearing the setting.", e);
                 clearUserDisabledHdrTypesLocked();
             }
         } else {
@@ -813,6 +817,17 @@
         }
     }
 
+    private void updateUserPreferredDisplayModeSettingsLocked() {
+        final float refreshRate = Settings.Global.getFloat(mContext.getContentResolver(),
+                Settings.Global.USER_PREFERRED_REFRESH_RATE, 0.0f);
+        final int height = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.USER_PREFERRED_RESOLUTION_HEIGHT, -1);
+        final int width = Settings.Global.getInt(mContext.getContentResolver(),
+                Settings.Global.USER_PREFERRED_RESOLUTION_WIDTH, -1);
+        Display.Mode mode = new Display.Mode(height, width, refreshRate);
+        mUserPreferredMode = isResolutionAndRefreshRateValid(mode) ? mode : null;
+    }
+
     private DisplayInfo getDisplayInfoForFrameRateOverride(DisplayEventReceiver.FrameRateOverride[]
             frameRateOverrides, DisplayInfo info, int callingUid) {
         float frameRateHz = 0;
@@ -1259,6 +1274,9 @@
             recordStableDisplayStatsIfNeededLocked(display);
             recordTopInsetLocked(display);
         }
+        if (mUserPreferredMode != null) {
+            device.setUserPreferredDisplayModeLocked(mUserPreferredMode);
+        }
         addDisplayPowerControllerLocked(display);
         mDisplayStates.append(displayId, Display.STATE_UNKNOWN);
 
@@ -1429,6 +1447,39 @@
         return mWideColorSpace.getId();
     }
 
+    void setUserPreferredDisplayModeInternal(Display.Mode mode) {
+        synchronized (mSyncRoot) {
+            if (Objects.equals(mUserPreferredMode, mode)) {
+                return;
+            }
+
+            if (mode != null && !isResolutionAndRefreshRateValid(mode)) {
+                throw new IllegalArgumentException("width, height and refresh rate of mode should "
+                        + "be greater than 0");
+            }
+            mUserPreferredMode = mode;
+
+            final int resolutionHeight = mode == null ? -1 : mode.getPhysicalHeight();
+            final int resolutionWidth = mode == null ? -1 : mode.getPhysicalWidth();
+            final float refreshRate = mode == null ? 0.0f : mode.getRefreshRate();
+            Settings.Global.putFloat(mContext.getContentResolver(),
+                    Settings.Global.USER_PREFERRED_REFRESH_RATE, refreshRate);
+            Settings.Global.putInt(mContext.getContentResolver(),
+                    Settings.Global.USER_PREFERRED_RESOLUTION_HEIGHT, resolutionHeight);
+            Settings.Global.putInt(mContext.getContentResolver(),
+                    Settings.Global.USER_PREFERRED_RESOLUTION_WIDTH, resolutionWidth);
+            mDisplayDeviceRepo.forEachLocked((DisplayDevice device) -> {
+                device.setUserPreferredDisplayModeLocked(mode);
+            });
+        }
+    }
+
+    private Display.Mode getUserPreferredDisplayModeInternal() {
+        synchronized (mSyncRoot) {
+            return mUserPreferredMode;
+        }
+    }
+
     void setShouldAlwaysRespectAppRequestedModeInternal(boolean enabled) {
         mDisplayModeDirector.setShouldAlwaysRespectAppRequestedMode(enabled);
     }
@@ -2019,6 +2070,10 @@
             pw.println("  mStableDisplaySize=" + mStableDisplaySize);
             pw.println("  mMinimumBrightnessCurve=" + mMinimumBrightnessCurve);
 
+            if (mUserPreferredMode != null) {
+                pw.println(mUserPreferredMode.toString());
+            }
+
             pw.println();
             if (!mAreUserDisabledHdrTypesAllowed) {
                 pw.println("  mUserDisabledHdrTypes: size=" + mUserDisabledHdrTypes.length);
@@ -2097,6 +2152,11 @@
         return floatArray;
     }
 
+    private static boolean isResolutionAndRefreshRateValid(Display.Mode mode) {
+        return mode.getPhysicalWidth() > 0 && mode.getPhysicalHeight() > 0
+                && mode.getRefreshRate() > 0.0f;
+    }
+
     /**
      * This is the object that everything in the display manager locks on.
      * We make it an inner class within the {@link DisplayManagerService} to so that it is
@@ -3084,6 +3144,29 @@
         }
 
         @Override // Binder call
+        public void setUserPreferredDisplayMode(Display.Mode mode) {
+            mContext.enforceCallingOrSelfPermission(
+                    Manifest.permission.WRITE_SECURE_SETTINGS,
+                    "Permission required to set the user preferred display mode.");
+            final long token = Binder.clearCallingIdentity();
+            try {
+                setUserPreferredDisplayModeInternal(mode);
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override // Binder call
+        public Display.Mode getUserPreferredDisplayMode() {
+            final long token = Binder.clearCallingIdentity();
+            try {
+                return getUserPreferredDisplayModeInternal();
+            } finally {
+                Binder.restoreCallingIdentity(token);
+            }
+        }
+
+        @Override // Binder call
         public void setShouldAlwaysRespectAppRequestedMode(boolean enabled) {
             mContext.enforceCallingOrSelfPermission(
                     Manifest.permission.OVERRIDE_DISPLAY_MODE_REQUESTS,
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index be4ec71..fce3fd53 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -97,8 +97,6 @@
     // specific display.
     private static final int GLOBAL_ID = -1;
 
-    private static final int INVALID_DISPLAY_MODE_ID = -1;
-
     private static final float FLOAT_TOLERANCE = RefreshRateRange.FLOAT_TOLERANCE;
 
     private final Object mLock = new Object();
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 67df565..dbe17b7 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -16,6 +16,8 @@
 
 package com.android.server.display;
 
+import static android.view.Display.Mode.INVALID_MODE_ID;
+
 import android.app.ActivityThread;
 import android.content.Context;
 import android.content.res.Resources;
@@ -65,8 +67,6 @@
 
     private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.emulator.circular";
 
-    private static final int NO_DISPLAY_MODE_ID = 0;
-
     private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>();
 
     private final Injector mInjector;
@@ -191,9 +191,11 @@
         // This is only set in the runnable returned from requestDisplayStateLocked.
         private float mBrightnessState = PowerManager.BRIGHTNESS_INVALID_FLOAT;
         private float mSdrBrightnessState = PowerManager.BRIGHTNESS_INVALID_FLOAT;
-        private int mDefaultModeId;
+        private int mDefaultModeId = INVALID_MODE_ID;
         private int mDefaultModeGroup;
-        private int mActiveModeId;
+        private int mUserPreferredModeId = INVALID_MODE_ID;
+        private Display.Mode mUserPreferredMode;
+        private int mActiveModeId = INVALID_MODE_ID;
         private DisplayModeDirector.DesiredDisplayModeSpecs mDisplayModeSpecs =
                 new DisplayModeDirector.DesiredDisplayModeSpecs();
         private boolean mDisplayModeSpecsInvalid;
@@ -323,7 +325,7 @@
 
             // Check whether SurfaceFlinger or the display device changed the active mode out from
             // under us.
-            if (mActiveModeId != NO_DISPLAY_MODE_ID
+            if (mActiveModeId != INVALID_MODE_ID
                     && mActiveModeId != activeRecord.mMode.getModeId()) {
                 Slog.d(TAG, "The active mode was changed from SurfaceFlinger or the display"
                         + " device to " + activeRecord.mMode);
@@ -334,12 +336,12 @@
 
             // Check whether surface flinger spontaneously changed display config specs out from
             // under us. If so, schedule a traversal to reapply our display config specs.
-            if (mDisplayModeSpecs.baseModeId != NO_DISPLAY_MODE_ID) {
+            if (mDisplayModeSpecs.baseModeId != INVALID_MODE_ID) {
                 int activeBaseMode = findMatchingModeIdLocked(modeSpecs.defaultMode);
                 // If we can't map the defaultMode index to a mode, then the physical display
                 // modes must have changed, and the code below for handling changes to the
                 // list of available modes will take care of updating display mode specs.
-                if (activeBaseMode == NO_DISPLAY_MODE_ID
+                if (activeBaseMode == INVALID_MODE_ID
                         || mDisplayModeSpecs.baseModeId != activeBaseMode
                         || mDisplayModeSpecs.primaryRefreshRateRange.min
                                 != modeSpecs.primaryRefreshRateMin
@@ -366,7 +368,7 @@
             }
 
             // For a new display, we need to initialize the default mode ID.
-            if (mDefaultModeId == NO_DISPLAY_MODE_ID) {
+            if (mDefaultModeId == INVALID_MODE_ID) {
                 mDefaultModeId = activeRecord.mMode.getModeId();
                 mDefaultModeGroup = mActiveSfDisplayMode.group;
             } else if (modesAdded && activeModeChanged) {
@@ -383,7 +385,7 @@
 
             // Determine whether the display mode specs' base mode is still there.
             if (mSupportedModes.indexOfKey(mDisplayModeSpecs.baseModeId) < 0) {
-                if (mDisplayModeSpecs.baseModeId != NO_DISPLAY_MODE_ID) {
+                if (mDisplayModeSpecs.baseModeId != INVALID_MODE_ID) {
                     Slog.w(TAG,
                             "DisplayModeSpecs base mode no longer available, using currently"
                                     + " active mode.");
@@ -392,13 +394,17 @@
                 mDisplayModeSpecsInvalid = true;
             }
 
+            if (mUserPreferredMode != null) {
+                mUserPreferredModeId = findUserPreferredModeIdLocked(mUserPreferredMode);
+            }
+
             // Determine whether the active mode is still there.
             if (mSupportedModes.indexOfKey(mActiveModeId) < 0) {
-                if (mActiveModeId != NO_DISPLAY_MODE_ID) {
+                if (mActiveModeId != INVALID_MODE_ID) {
                     Slog.w(TAG, "Active display mode no longer available, reverting to default"
                             + " mode.");
                 }
-                mActiveModeId = mDefaultModeId;
+                mActiveModeId = getPreferredModeId();
             }
 
             // Schedule traversals so that we apply pending changes.
@@ -414,6 +420,12 @@
             return mDisplayDeviceConfig;
         }
 
+        private int getPreferredModeId() {
+            return mUserPreferredModeId != INVALID_MODE_ID
+                    ? mUserPreferredModeId
+                    : mDefaultModeId;
+        }
+
         private void loadDisplayDeviceConfig() {
             // Load display device config
             final Context context = getOverlayContext();
@@ -561,7 +573,7 @@
                 mInfo.width = mActiveSfDisplayMode.width;
                 mInfo.height = mActiveSfDisplayMode.height;
                 mInfo.modeId = mActiveModeId;
-                mInfo.defaultModeId = mDefaultModeId;
+                mInfo.defaultModeId = getPreferredModeId();
                 mInfo.supportedModes = getDisplayModes(mSupportedModes);
                 mInfo.colorMode = mActiveColorMode;
                 mInfo.allmSupported = mAllmSupported;
@@ -823,6 +835,17 @@
         }
 
         @Override
+        public void setUserPreferredDisplayModeLocked(Display.Mode mode) {
+            final int oldModeId = getPreferredModeId();
+            mUserPreferredMode = mode;
+            mUserPreferredModeId = findUserPreferredModeIdLocked(mode);
+
+            if (oldModeId != getPreferredModeId()) {
+                updateDeviceInfoLocked();
+            }
+        }
+
+        @Override
         public void setRequestedColorModeLocked(int colorMode) {
             requestColorModeLocked(colorMode);
         }
@@ -903,7 +926,7 @@
             }
             mActiveSfDisplayMode = getModeById(mSfDisplayModes, activeSfModeId);
             mActiveModeId = findMatchingModeIdLocked(activeSfModeId);
-            if (mActiveModeId == NO_DISPLAY_MODE_ID) {
+            if (mActiveModeId == INVALID_MODE_ID) {
                 Slog.w(TAG, "In unknown mode after setting allowed modes"
                         + ", activeModeId=" + activeSfModeId);
             }
@@ -988,6 +1011,7 @@
             pw.println("mActiveModeId=" + mActiveModeId);
             pw.println("mActiveColorMode=" + mActiveColorMode);
             pw.println("mDefaultModeId=" + mDefaultModeId);
+            pw.println("mUserPreferredModeId=" + mUserPreferredModeId);
             pw.println("mState=" + Display.stateToString(mState));
             pw.println("mBrightnessState=" + mBrightnessState);
             pw.println("mBacklightAdapter=" + mBacklightAdapter);
@@ -1010,13 +1034,12 @@
         }
 
         private int findDisplayModeIdLocked(int modeId, int modeGroup) {
-            int matchingModeId = SurfaceControl.DisplayMode.INVALID_DISPLAY_MODE_ID;
+            int matchingModeId = INVALID_MODE_ID;
             DisplayModeRecord record = mSupportedModes.get(modeId);
             if (record != null) {
                 for (SurfaceControl.DisplayMode mode : mSfDisplayModes) {
                     if (record.hasMatchingMode(mode)) {
-                        if (matchingModeId
-                                == SurfaceControl.DisplayMode.INVALID_DISPLAY_MODE_ID) {
+                        if (matchingModeId == INVALID_MODE_ID) {
                             matchingModeId = mode.id;
                         }
 
@@ -1030,11 +1053,25 @@
             return matchingModeId;
         }
 
+        private int findUserPreferredModeIdLocked(Display.Mode userPreferredMode) {
+            if (userPreferredMode != null) {
+                for (int i = 0; i < mSupportedModes.size(); i++) {
+                    Display.Mode supportedMode = mSupportedModes.valueAt(i).mMode;
+                    if (userPreferredMode.matches(supportedMode.getPhysicalWidth(),
+                            supportedMode.getPhysicalHeight(),
+                            supportedMode.getRefreshRate())) {
+                        return supportedMode.getModeId();
+                    }
+                }
+            }
+            return INVALID_MODE_ID;
+        }
+
         private int findMatchingModeIdLocked(int sfModeId) {
             SurfaceControl.DisplayMode mode = getModeById(mSfDisplayModes, sfModeId);
             if (mode == null) {
                 Slog.e(TAG, "Invalid display mode ID " + sfModeId);
-                return NO_DISPLAY_MODE_ID;
+                return INVALID_MODE_ID;
             }
             for (int i = 0; i < mSupportedModes.size(); i++) {
                 DisplayModeRecord record = mSupportedModes.valueAt(i);
@@ -1042,7 +1079,7 @@
                     return record.mMode.getModeId();
                 }
             }
-            return NO_DISPLAY_MODE_ID;
+            return INVALID_MODE_ID;
         }
 
         private void updateDeviceInfoLocked() {
diff --git a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
index 28cdd63..b5ad459 100644
--- a/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/display/LocalDisplayAdapterTest.java
@@ -336,6 +336,62 @@
     }
 
     @Test
+    public void testAfterDisplayChange_DefaultDisplayModeIsUpdated() throws Exception {
+        SurfaceControl.DisplayMode displayMode = createFakeDisplayMode(0, 1920, 1080, 60f);
+        SurfaceControl.DisplayMode[] modes =
+                new SurfaceControl.DisplayMode[]{displayMode};
+        FakeDisplay display = new FakeDisplay(PORT_A, modes, 0);
+        setUpDisplay(display);
+        updateAvailableDisplays();
+        mAdapter.registerLocked();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+        assertThat(mListener.changedDisplays).isEmpty();
+
+        DisplayDeviceInfo displayDeviceInfo = mListener.addedDisplays.get(
+                0).getDisplayDeviceInfoLocked();
+
+        assertThat(displayDeviceInfo.supportedModes.length).isEqualTo(modes.length);
+        assertModeIsSupported(displayDeviceInfo.supportedModes, displayMode);
+
+        Display.Mode defaultMode = getModeById(displayDeviceInfo, displayDeviceInfo.defaultModeId);
+        assertThat(matches(defaultMode, displayMode)).isTrue();
+
+        // Set the display mode to an unsupported mode
+        SurfaceControl.DisplayMode displayMode2 = createFakeDisplayMode(1, 1920, 1080, 120f);
+        mListener.addedDisplays.get(0).setUserPreferredDisplayModeLocked(
+                new Display.Mode(displayMode2.width, displayMode2.height,
+                        displayMode2.refreshRate));
+        updateAvailableDisplays();
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+        defaultMode = getModeById(displayDeviceInfo, displayDeviceInfo.defaultModeId);
+        assertThat(matches(defaultMode, displayMode2)).isFalse();
+
+        // Change the display
+        modes = new SurfaceControl.DisplayMode[]{displayMode, displayMode2};
+        display.dynamicInfo.supportedDisplayModes = modes;
+        setUpDisplay(display);
+        mInjector.getTransmitter().sendHotplug(display, /* connected */ true);
+        waitForHandlerToComplete(mHandler, HANDLER_WAIT_MS);
+
+        assertTrue(mListener.traversalRequested);
+        assertThat(mListener.addedDisplays.size()).isEqualTo(1);
+        assertThat(mListener.changedDisplays.size()).isEqualTo(1);
+
+        DisplayDevice displayDevice = mListener.changedDisplays.get(0);
+        displayDevice.applyPendingDisplayDeviceInfoChangesLocked();
+        displayDeviceInfo = mListener.addedDisplays.get(0).getDisplayDeviceInfoLocked();
+
+        assertThat(displayDeviceInfo.supportedModes.length).isEqualTo(modes.length);
+        assertModeIsSupported(displayDeviceInfo.supportedModes, displayMode);
+        assertModeIsSupported(displayDeviceInfo.supportedModes, displayMode2);
+
+        defaultMode = getModeById(displayDeviceInfo, displayDeviceInfo.defaultModeId);
+        assertThat(matches(defaultMode, displayMode2)).isTrue();
+    }
+
+    @Test
     public void testAfterDisplayChange_DisplayModesAreUpdated() throws Exception {
         SurfaceControl.DisplayMode displayMode = createFakeDisplayMode(0, 1920, 1080, 60f);
         SurfaceControl.DisplayMode[] modes =
@@ -356,12 +412,10 @@
         assertModeIsSupported(displayDeviceInfo.supportedModes, displayMode);
 
         Display.Mode defaultMode = getModeById(displayDeviceInfo, displayDeviceInfo.defaultModeId);
-        assertThat(defaultMode.matches(displayMode.width, displayMode.height,
-                displayMode.refreshRate)).isTrue();
+        assertThat(matches(defaultMode, displayMode)).isTrue();
 
         Display.Mode activeMode = getModeById(displayDeviceInfo, displayDeviceInfo.modeId);
-        assertThat(activeMode.matches(displayMode.width, displayMode.height,
-                displayMode.refreshRate)).isTrue();
+        assertThat(matches(activeMode, displayMode)).isTrue();
 
         // Change the display
         SurfaceControl.DisplayMode addedDisplayInfo = createFakeDisplayMode(1, 3840, 2160, 60f);
@@ -385,12 +439,10 @@
         assertModeIsSupported(displayDeviceInfo.supportedModes, addedDisplayInfo);
 
         activeMode = getModeById(displayDeviceInfo, displayDeviceInfo.modeId);
-        assertThat(activeMode.matches(addedDisplayInfo.width, addedDisplayInfo.height,
-                addedDisplayInfo.refreshRate)).isTrue();
+        assertThat(matches(activeMode, addedDisplayInfo)).isTrue();
 
         defaultMode = getModeById(displayDeviceInfo, displayDeviceInfo.defaultModeId);
-        assertThat(defaultMode.matches(addedDisplayInfo.width, addedDisplayInfo.height,
-                addedDisplayInfo.refreshRate)).isTrue();
+        assertThat(matches(defaultMode, addedDisplayInfo)).isTrue();
     }
 
     @Test
@@ -918,4 +970,9 @@
         });
         return mockArray;
     }
+
+    private boolean matches(Display.Mode a, SurfaceControl.DisplayMode b) {
+        return a.getPhysicalWidth() == b.width && a.getPhysicalHeight() == b.height
+                && Float.floatToIntBits(a.getRefreshRate()) == Float.floatToIntBits(b.refreshRate);
+    }
 }