Merge "Simplify IMMS#shouldShowImeSwitcherLocked()" into main
diff --git a/api/StubLibraries.bp b/api/StubLibraries.bp
index 13d6ae5..6cfd2e0 100644
--- a/api/StubLibraries.bp
+++ b/api/StubLibraries.bp
@@ -44,8 +44,8 @@
             removed_api_file: ":non-updatable-removed.txt",
         },
         last_released: {
-            api_file: ":android-non-updatable.api.public.latest",
-            removed_api_file: ":android-non-updatable-removed.api.public.latest",
+            api_file: ":android-non-updatable.api.combined.public.latest",
+            removed_api_file: ":android-non-updatable-removed.api.combined.public.latest",
             baseline_file: ":android-non-updatable-incompatibilities.api.public.latest",
         },
         api_lint: {
@@ -124,8 +124,8 @@
             removed_api_file: ":non-updatable-system-removed.txt",
         },
         last_released: {
-            api_file: ":android-non-updatable.api.system.latest",
-            removed_api_file: ":android-non-updatable-removed.api.system.latest",
+            api_file: ":android-non-updatable.api.combined.system.latest",
+            removed_api_file: ":android-non-updatable-removed.api.combined.system.latest",
             baseline_file: ":android-non-updatable-incompatibilities.api.system.latest",
         },
         api_lint: {
@@ -263,8 +263,8 @@
             removed_api_file: ":non-updatable-module-lib-removed.txt",
         },
         last_released: {
-            api_file: ":android-non-updatable.api.module-lib.latest",
-            removed_api_file: ":android-non-updatable-removed.api.module-lib.latest",
+            api_file: ":android-non-updatable.api.combined.module-lib.latest",
+            removed_api_file: ":android-non-updatable-removed.api.combined.module-lib.latest",
             baseline_file: ":android-non-updatable-incompatibilities.api.module-lib.latest",
         },
         api_lint: {
diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java
index d4812dd..76c1ed6 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -18,6 +18,7 @@
 
 import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN;
 import static android.app.ConfigurationController.createNewConfigAndUpdateIfNotNull;
+import static android.app.Flags.skipBgMemTrimOnFgApp;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 import static android.app.servertransaction.ActivityLifecycleItem.ON_CREATE;
@@ -7078,6 +7079,11 @@
         if (DEBUG_MEMORY_TRIM) Slog.v(TAG, "Trimming memory to level: " + level);
 
         try {
+            if (skipBgMemTrimOnFgApp()
+                    && mLastProcessState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
+                    && level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
+                return;
+            }
             if (level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE) {
                 PropertyInvalidatedCache.onTrimMemory();
             }
diff --git a/core/java/android/app/activity_manager.aconfig b/core/java/android/app/activity_manager.aconfig
index bb24fd1..fa646a7 100644
--- a/core/java/android/app/activity_manager.aconfig
+++ b/core/java/android/app/activity_manager.aconfig
@@ -70,3 +70,14 @@
          purpose: PURPOSE_BUGFIX
      }
 }
+
+flag {
+     namespace: "backstage_power"
+     name: "skip_bg_mem_trim_on_fg_app"
+     description: "Skip background memory trim event on foreground processes."
+     is_fixed_read_only: true
+     bug: "308927629"
+     metadata {
+         purpose: PURPOSE_BUGFIX
+     }
+}
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 4154e66..2574a54 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -115,6 +115,16 @@
 }
 
 flag {
+  name: "hsum_unlock_notification_fix"
+  namespace: "enterprise"
+  description: "Using the right userId when starting the work profile unlock flow "
+  bug: "327350831"
+  metadata {
+      purpose: PURPOSE_BUGFIX
+  }
+}
+
+flag {
   name: "dumpsys_policy_engine_migration_enabled"
   namespace: "enterprise"
   description: "Update DumpSys to include information about migrated APIs in DPE"
@@ -336,3 +346,13 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+  name: "onboarding_consentless_bugreports"
+  namespace: "enterprise"
+  description: "Allow subsequent bugreports to skip user consent within a time frame"
+  bug: "340439309"
+  metadata {
+    purpose: PURPOSE_BUGFIX
+  }
+}
diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java
index 8220313..57ee622 100644
--- a/core/java/android/content/pm/CrossProfileApps.java
+++ b/core/java/android/content/pm/CrossProfileApps.java
@@ -326,6 +326,7 @@
      * @return whether the specified user is a profile.
      */
     @FlaggedApi(FLAG_ALLOW_QUERYING_PROFILE_TYPE)
+    @SuppressWarnings("UserHandleName")
     public boolean isProfile(@NonNull UserHandle userHandle) {
         // Note that this is not a security check, but rather a check for correct use.
         // The actual security check is performed by UserManager.
@@ -343,6 +344,7 @@
      * @return whether the specified user is a managed profile.
      */
     @FlaggedApi(FLAG_ALLOW_QUERYING_PROFILE_TYPE)
+    @SuppressWarnings("UserHandleName")
     public boolean isManagedProfile(@NonNull UserHandle userHandle) {
         // Note that this is not a security check, but rather a check for correct use.
         // The actual security check is performed by UserManager.
diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java
index 885f4c5..982224b 100644
--- a/core/java/android/content/res/Configuration.java
+++ b/core/java/android/content/res/Configuration.java
@@ -806,7 +806,7 @@
      *
      * <aside class="note"><b>Note:</b> If the app targets
      * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}
-     * or after, The width measurement reflects the window size without excluding insets.
+     * or after, the width measurement reflects the window size without excluding insets.
      * Otherwise, the measurement excludes window insets even when the app is displayed edge to edge
      * using {@link android.view.Window#setDecorFitsSystemWindows(boolean)
      * Window#setDecorFitsSystemWindows(boolean)}.</aside>
diff --git a/core/java/android/os/BugreportManager.java b/core/java/android/os/BugreportManager.java
index 960e84d..a818df5 100644
--- a/core/java/android/os/BugreportManager.java
+++ b/core/java/android/os/BugreportManager.java
@@ -252,7 +252,8 @@
                     params.getMode(),
                     params.getFlags(),
                     dsListener,
-                    isScreenshotRequested);
+                    isScreenshotRequested,
+                    /* skipUserConsent = */ false);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         } catch (FileNotFoundException e) {
@@ -313,6 +314,7 @@
                     bugreportFd.getFileDescriptor(),
                     bugreportFile,
                     /* keepBugreportOnRetrieval = */ false,
+                    /* skipUserConsent = */ false,
                     dsListener);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java
index 4475418..cf1bc4f2 100644
--- a/core/java/android/view/Display.java
+++ b/core/java/android/view/Display.java
@@ -916,6 +916,12 @@
      *         {@code getWindowManager()} or {@code getSystemService(Context.WINDOW_SERVICE)}), the
      *         size of the current app window is returned. As a result, in multi-window mode, the
      *         returned size can be smaller than the size of the device screen.
+     *         The returned window size can vary depending on API level:
+     *         <ul>
+     *             <li>API level 35 and above, the window size will be returned.
+     *             <li>API level 34 and below, the window size minus system decoration areas and
+     *             display cutout is returned.
+     *         </ul>
      *     <li>If size is requested from a non-activity context (for example, the application
      *         context, where the WindowManager is accessed by
      *         {@code getApplicationContext().getSystemService(Context.WINDOW_SERVICE)}), the
@@ -924,9 +930,10 @@
      *             <li>API level 29 and below &mdash; The size of the entire display (based on
      *                 current rotation) minus system decoration areas is returned.
      *             <li>API level 30 and above &mdash; The size of the top running activity in the
-     *                 current process is returned. If the current process has no running
-     *                 activities, the size of the device default display, including system
-     *                 decoration areas, is returned.
+     *                 current process is returned, system decoration areas exclusion follows the
+     *                 behavior defined above, based on the caller's API level. If the current
+     *                 process has no running activities, the size of the device default display,
+     *                 including system decoration areas, is returned.
      *         </ul>
      * </ul>
      *
@@ -1223,7 +1230,7 @@
     public Mode[] getSupportedModes() {
         synchronized (mLock) {
             updateDisplayInfoLocked();
-            final Display.Mode[] modes = mDisplayInfo.supportedModes;
+            final Display.Mode[] modes = mDisplayInfo.appsSupportedModes;
             return Arrays.copyOf(modes, modes.length);
         }
     }
@@ -2206,6 +2213,7 @@
         @NonNull
         @HdrCapabilities.HdrType
         private final int[] mSupportedHdrTypes;
+        private final boolean mIsSynthetic;
 
         /**
          * @hide
@@ -2219,13 +2227,6 @@
         /**
          * @hide
          */
-        public Mode(int width, int height, float refreshRate, float vsyncRate) {
-            this(INVALID_MODE_ID, width, height, refreshRate, vsyncRate, new float[0], new int[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, refreshRate, new float[0], new int[0]);
@@ -2246,11 +2247,21 @@
          */
         public Mode(int modeId, int width, int height, float refreshRate, float vsyncRate,
                 float[] alternativeRefreshRates, @HdrCapabilities.HdrType int[] supportedHdrTypes) {
+            this(modeId, width, height, refreshRate, vsyncRate, false, alternativeRefreshRates,
+                    supportedHdrTypes);
+        }
+        /**
+         * @hide
+         */
+        public Mode(int modeId, int width, int height, float refreshRate, float vsyncRate,
+                boolean isSynthetic, float[] alternativeRefreshRates,
+                @HdrCapabilities.HdrType int[] supportedHdrTypes) {
             mModeId = modeId;
             mWidth = width;
             mHeight = height;
             mPeakRefreshRate = refreshRate;
             mVsyncRate = vsyncRate;
+            mIsSynthetic = isSynthetic;
             mAlternativeRefreshRates =
                     Arrays.copyOf(alternativeRefreshRates, alternativeRefreshRates.length);
             Arrays.sort(mAlternativeRefreshRates);
@@ -2315,6 +2326,15 @@
         }
 
         /**
+         * Returns true if mode is synthetic and does not have corresponding
+         * SurfaceControl.DisplayMode
+         * @hide
+         */
+        public boolean isSynthetic() {
+            return mIsSynthetic;
+        }
+
+        /**
          * Returns an array of refresh rates which can be switched to seamlessly.
          * <p>
          * A seamless switch is one without visual interruptions, such as a black screen for
@@ -2449,6 +2469,7 @@
                     .append(", height=").append(mHeight)
                     .append(", fps=").append(mPeakRefreshRate)
                     .append(", vsync=").append(mVsyncRate)
+                    .append(", synthetic=").append(mIsSynthetic)
                     .append(", alternativeRefreshRates=")
                     .append(Arrays.toString(mAlternativeRefreshRates))
                     .append(", supportedHdrTypes=")
@@ -2464,7 +2485,7 @@
 
         private Mode(Parcel in) {
             this(in.readInt(), in.readInt(), in.readInt(), in.readFloat(), in.readFloat(),
-                    in.createFloatArray(), in.createIntArray());
+                    in.readBoolean(), in.createFloatArray(), in.createIntArray());
         }
 
         @Override
@@ -2474,6 +2495,7 @@
             out.writeInt(mHeight);
             out.writeFloat(mPeakRefreshRate);
             out.writeFloat(mVsyncRate);
+            out.writeBoolean(mIsSynthetic);
             out.writeFloatArray(mAlternativeRefreshRates);
             out.writeIntArray(mSupportedHdrTypes);
         }
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 5654bc1..da86e2d 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -211,6 +211,12 @@
      */
     public Display.Mode[] supportedModes = Display.Mode.EMPTY_ARRAY;
 
+    /**
+     * The supported modes that will be exposed externally.
+     * Might have different set of modes that supportedModes for VRR displays
+     */
+    public Display.Mode[] appsSupportedModes = Display.Mode.EMPTY_ARRAY;
+
     /** The active color mode. */
     public int colorMode;
 
@@ -429,6 +435,7 @@
                 && defaultModeId == other.defaultModeId
                 && userPreferredModeId == other.userPreferredModeId
                 && Arrays.equals(supportedModes, other.supportedModes)
+                && Arrays.equals(appsSupportedModes, other.appsSupportedModes)
                 && colorMode == other.colorMode
                 && Arrays.equals(supportedColorModes, other.supportedColorModes)
                 && Objects.equals(hdrCapabilities, other.hdrCapabilities)
@@ -488,6 +495,8 @@
         defaultModeId = other.defaultModeId;
         userPreferredModeId = other.userPreferredModeId;
         supportedModes = Arrays.copyOf(other.supportedModes, other.supportedModes.length);
+        appsSupportedModes = Arrays.copyOf(
+                other.appsSupportedModes, other.appsSupportedModes.length);
         colorMode = other.colorMode;
         supportedColorModes = Arrays.copyOf(
                 other.supportedColorModes, other.supportedColorModes.length);
@@ -545,6 +554,11 @@
         for (int i = 0; i < nModes; i++) {
             supportedModes[i] = Display.Mode.CREATOR.createFromParcel(source);
         }
+        int nAppModes = source.readInt();
+        appsSupportedModes = new Display.Mode[nAppModes];
+        for (int i = 0; i < nAppModes; i++) {
+            appsSupportedModes[i] = Display.Mode.CREATOR.createFromParcel(source);
+        }
         colorMode = source.readInt();
         int nColorModes = source.readInt();
         supportedColorModes = new int[nColorModes];
@@ -611,6 +625,10 @@
         for (int i = 0; i < supportedModes.length; i++) {
             supportedModes[i].writeToParcel(dest, flags);
         }
+        dest.writeInt(appsSupportedModes.length);
+        for (int i = 0; i < appsSupportedModes.length; i++) {
+            appsSupportedModes[i].writeToParcel(dest, flags);
+        }
         dest.writeInt(colorMode);
         dest.writeInt(supportedColorModes.length);
         for (int i = 0; i < supportedColorModes.length; i++) {
@@ -849,8 +867,10 @@
         sb.append(defaultModeId);
         sb.append(", userPreferredModeId ");
         sb.append(userPreferredModeId);
-        sb.append(", modes ");
+        sb.append(", supportedModes ");
         sb.append(Arrays.toString(supportedModes));
+        sb.append(", appsSupportedModes ");
+        sb.append(Arrays.toString(appsSupportedModes));
         sb.append(", hdrCapabilities ");
         sb.append(hdrCapabilities);
         sb.append(", userDisabledHdrTypes ");
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index d7f2b01..fd10a1f 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -303,7 +303,7 @@
     }
 
     /** Not running an animation. */
-    @VisibleForTesting
+    @VisibleForTesting(visibility = PACKAGE)
     public static final int ANIMATION_TYPE_NONE = -1;
 
     /** Running animation will show insets */
@@ -317,7 +317,7 @@
     public static final int ANIMATION_TYPE_USER = 2;
 
     /** Running animation will resize insets */
-    @VisibleForTesting
+    @VisibleForTesting(visibility = PACKAGE)
     public static final int ANIMATION_TYPE_RESIZE = 3;
 
     @Retention(RetentionPolicy.SOURCE)
@@ -1714,7 +1714,7 @@
         mImeSourceConsumer.onWindowFocusLost();
     }
 
-    @VisibleForTesting
+    @VisibleForTesting(visibility = PACKAGE)
     public @AnimationType int getAnimationType(@InsetsType int type) {
         for (int i = mRunningAnimations.size() - 1; i >= 0; i--) {
             InsetsAnimationControlRunner control = mRunningAnimations.get(i).runner;
diff --git a/core/java/android/view/InsetsSourceConsumer.java b/core/java/android/view/InsetsSourceConsumer.java
index fdb2a6e..6c670f5 100644
--- a/core/java/android/view/InsetsSourceConsumer.java
+++ b/core/java/android/view/InsetsSourceConsumer.java
@@ -17,6 +17,7 @@
 package android.view;
 
 import static android.view.InsetsController.ANIMATION_TYPE_NONE;
+import static android.view.InsetsController.ANIMATION_TYPE_RESIZE;
 import static android.view.InsetsController.AnimationType;
 import static android.view.InsetsController.DEBUG;
 import static android.view.InsetsSourceConsumerProto.ANIMATION_STATE;
@@ -31,6 +32,7 @@
 
 import android.annotation.IntDef;
 import android.annotation.Nullable;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.util.Log;
 import android.util.proto.ProtoOutputStream;
@@ -179,10 +181,11 @@
                     mController.notifyVisibilityChanged();
                 }
 
-                // If we have a new leash, make sure visibility is up-to-date, even though we
-                // didn't want to run an animation above.
-                if (mController.getAnimationType(mType) == ANIMATION_TYPE_NONE) {
-                    applyRequestedVisibilityToControl();
+                // If there is no animation controlling the leash, make sure the visibility and the
+                // position is up-to-date.
+                final int animType = mController.getAnimationType(mType);
+                if (animType == ANIMATION_TYPE_NONE || animType == ANIMATION_TYPE_RESIZE) {
+                    applyRequestedVisibilityAndPositionToControl();
                 }
 
                 // Remove the surface that owned by last control when it lost.
@@ -371,21 +374,27 @@
         if (DEBUG) Log.d(TAG, "updateSource: " + newSource);
     }
 
-    private void applyRequestedVisibilityToControl() {
-        if (mSourceControl == null || mSourceControl.getLeash() == null) {
+    private void applyRequestedVisibilityAndPositionToControl() {
+        if (mSourceControl == null) {
+            return;
+        }
+        final SurfaceControl leash = mSourceControl.getLeash();
+        if (leash == null) {
             return;
         }
 
         final boolean requestedVisible = (mController.getRequestedVisibleTypes() & mType) != 0;
+        final Point surfacePosition = mSourceControl.getSurfacePosition();
         try (Transaction t = mTransactionSupplier.get()) {
             if (DEBUG) Log.d(TAG, "applyRequestedVisibilityToControl: " + requestedVisible);
             if (requestedVisible) {
-                t.show(mSourceControl.getLeash());
+                t.show(leash);
             } else {
-                t.hide(mSourceControl.getLeash());
+                t.hide(leash);
             }
             // Ensure the alpha value is aligned with the actual requested visibility.
-            t.setAlpha(mSourceControl.getLeash(), requestedVisible ? 1 : 0);
+            t.setAlpha(leash, requestedVisible ? 1 : 0);
+            t.setPosition(leash, surfacePosition.x, surfacePosition.y);
             t.apply();
         }
         onPerceptible(requestedVisible);
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index f22e8f5..0f54940b 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -781,7 +781,7 @@
      * <p>
      * The metrics describe the size of the area the window would occupy with
      * {@link LayoutParams#MATCH_PARENT MATCH_PARENT} width and height, and the {@link WindowInsets}
-     * such a window would have.
+     * such a window would have. The {@link WindowInsets} are not deducted from the bounds.
      * <p>
      * The value of this is based on the <b>current</b> windowing state of the system.
      *
@@ -811,7 +811,7 @@
      * <p>
      * The metrics describe the size of the largest potential area the window might occupy with
      * {@link LayoutParams#MATCH_PARENT MATCH_PARENT} width and height, and the {@link WindowInsets}
-     * such a window would have.
+     * such a window would have. The {@link WindowInsets} are not deducted from the bounds.
      * <p>
      * Note that this might still be smaller than the size of the physical display if certain areas
      * of the display are not available to windows created in this {@link Context}.
@@ -4264,11 +4264,9 @@
          *         no letterbox is applied."/>
          *
          * <p>
-         * A cutout in the corner is considered to be on the short edge: <br/>
-         * <img src="{@docRoot}reference/android/images/display_cutout/short_edge/fullscreen_corner_no_letterbox.png"
-         * height="720"
-         * alt="Screenshot of a fullscreen activity on a display with a cutout in the corner in
-         *         portrait, no letterbox is applied."/>
+         * A cutout in the corner can be considered to be on different edge in different device
+         * rotations. This behavior may vary from device to device. Use this flag is possible to
+         * letterbox your app if the display cutout is at corner.
          *
          * <p>
          * On the other hand, should the cutout be on the long edge of the display, a letterbox will
diff --git a/core/java/android/view/WindowMetrics.java b/core/java/android/view/WindowMetrics.java
index 26298bc..8bcc9de 100644
--- a/core/java/android/view/WindowMetrics.java
+++ b/core/java/android/view/WindowMetrics.java
@@ -101,9 +101,13 @@
      * Returns the bounds of the area associated with this window or {@code UiContext}.
      * <p>
      * <b>Note that the size of the reported bounds can have different size than
-     * {@link Display#getSize(Point)}.</b> This method reports the window size including all system
-     * bar areas, while {@link Display#getSize(Point)} reports the area excluding navigation bars
-     * and display cutout areas. The value reported by {@link Display#getSize(Point)} can be
+     * {@link Display#getSize(Point)} based on your target API level and calling context.</b>
+     * This method reports the window size including all system
+     * bar areas, while {@link Display#getSize(Point)} can report the area excluding navigation bars
+     * and display cutout areas depending on the calling context and target SDK level. Please refer
+     * to {@link Display#getSize(Point)} for details.
+     * <p>
+     * The value reported by {@link Display#getSize(Point)} excluding system decoration areas can be
      * obtained by using:
      * <pre class="prettyprint">
      * final WindowMetrics metrics = windowManager.getCurrentWindowMetrics();
diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
index da2bf9d..4de3a7b 100644
--- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
+++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig
@@ -124,6 +124,7 @@
     namespace: "accessibility"
     name: "add_type_window_control"
     is_exported: true
+    is_fixed_read_only: true
     description: "adds new TYPE_WINDOW_CONTROL to AccessibilityWindowInfo for detecting Window Decorations"
     bug: "320445550"
 }
diff --git a/core/java/android/window/BackProgressAnimator.java b/core/java/android/window/BackProgressAnimator.java
index 163e43a..6fe0784 100644
--- a/core/java/android/window/BackProgressAnimator.java
+++ b/core/java/android/window/BackProgressAnimator.java
@@ -114,7 +114,6 @@
      *                 dispatches as the progress animation updates.
      */
     public void onBackStarted(BackMotionEvent event, ProgressCallback callback) {
-        reset();
         mLastBackEvent = event;
         mCallback = callback;
         mBackAnimationInProgress = true;
diff --git a/core/java/android/window/TaskSnapshot.java b/core/java/android/window/TaskSnapshot.java
index a2e3d40..f0144cb 100644
--- a/core/java/android/window/TaskSnapshot.java
+++ b/core/java/android/window/TaskSnapshot.java
@@ -33,6 +33,8 @@
 import android.view.Surface;
 import android.view.WindowInsetsController;
 
+import com.android.window.flags.Flags;
+
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -334,7 +336,8 @@
      */
     public synchronized void removeReference(@ReferenceFlags int usage) {
         mInternalReferences &= ~usage;
-        if (mInternalReferences == 0 && mSnapshot != null && !mSnapshot.isClosed()) {
+        if (Flags.releaseSnapshotAggressively() && mInternalReferences == 0 && mSnapshot != null
+                && !mSnapshot.isClosed()) {
             mSnapshot.close();
         }
     }
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index ee3e34f..f08f5b8 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -163,4 +163,12 @@
   metadata {
     purpose: PURPOSE_BUGFIX
   }
+}
+
+flag {
+    name: "release_snapshot_aggressively"
+    namespace: "windowing_frontend"
+    description: "Actively release task snapshot memory"
+    bug: "238206323"
+    is_fixed_read_only: true
 }
\ No newline at end of file
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 5fa13ba..405324b 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -2589,6 +2589,8 @@
                  <li>The framework will set {@link android.R.attr#statusBarColor},
                  {@link android.R.attr#navigationBarColor}, and
                  {@link android.R.attr#navigationBarDividerColor} to transparent.
+                 <li>The frameworks will send Configuration no longer considering system insets.
+                 The Configuration will be stable regardless of the system insets change.
              </ul>
 
              <p>If this is true, the edge-to-edge enforcement won't be applied. However, this
diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java
index fa08658..bdd3c73 100644
--- a/media/java/android/media/LoudnessCodecDispatcher.java
+++ b/media/java/android/media/LoudnessCodecDispatcher.java
@@ -16,6 +16,9 @@
 
 package android.media;
 
+import static android.media.MediaFormat.KEY_AAC_DRC_ALBUM_MODE;
+import static android.media.MediaFormat.KEY_AAC_DRC_ATTENUATION_FACTOR;
+import static android.media.MediaFormat.KEY_AAC_DRC_BOOST_FACTOR;
 import static android.media.MediaFormat.KEY_AAC_DRC_EFFECT_TYPE;
 import static android.media.MediaFormat.KEY_AAC_DRC_HEAVY_COMPRESSION;
 import static android.media.MediaFormat.KEY_AAC_DRC_TARGET_REFERENCE_LEVEL;
@@ -142,6 +145,18 @@
                 filteredBundle.putInt(KEY_AAC_DRC_EFFECT_TYPE,
                         bundle.getInt(KEY_AAC_DRC_EFFECT_TYPE));
             }
+            if (bundle.containsKey(KEY_AAC_DRC_BOOST_FACTOR)) {
+                filteredBundle.putInt(KEY_AAC_DRC_BOOST_FACTOR,
+                        bundle.getInt(KEY_AAC_DRC_BOOST_FACTOR));
+            }
+            if (bundle.containsKey(KEY_AAC_DRC_ATTENUATION_FACTOR)) {
+                filteredBundle.putInt(KEY_AAC_DRC_ATTENUATION_FACTOR,
+                        bundle.getInt(KEY_AAC_DRC_ATTENUATION_FACTOR));
+            }
+            if (bundle.containsKey(KEY_AAC_DRC_ALBUM_MODE)) {
+                filteredBundle.putInt(KEY_AAC_DRC_ALBUM_MODE,
+                        bundle.getInt(KEY_AAC_DRC_ALBUM_MODE));
+            }
 
             return filteredBundle;
         }
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index 223b432c..4059291 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -109,7 +109,7 @@
         try {
             final Callback c = Objects.requireNonNull(callback);
             if (handler == null) {
-                handler = new Handler();
+                handler = new Handler(mContext.getMainLooper());
             }
             mCallbacks.put(c, new CallbackRecord(c, handler));
         } catch (NullPointerException e) {
diff --git a/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm b/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm
index 03b5c19..723c187 100644
--- a/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm
+++ b/packages/InputDevices/res/raw/keyboard_layout_french_ca.kcm
@@ -348,13 +348,13 @@
     label:                              ','
     base:                               ','
     shift:                              '\''
-    ralt:                               '_'
+    ralt:                               '\u00af'
 }
 
 key PERIOD {
     label:                              '.'
     base:                               '.'
-    ralt:                               '-'
+    ralt:                               '\u00ad'
 }
 
 key SLASH {
diff --git a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
index 4579168..050a370 100644
--- a/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
+++ b/packages/Shell/tests/src/com/android/shell/BugreportReceiverTest.java
@@ -200,7 +200,7 @@
             mBugreportFd = ParcelFileDescriptor.dup(invocation.getArgument(2));
             return null;
         }).when(mMockIDumpstate).startBugreport(anyInt(), any(), any(), any(), anyInt(), anyInt(),
-                any(), anyBoolean());
+                any(), anyBoolean(), anyBoolean());
 
         setWarningState(mContext, STATE_HIDE);
 
@@ -543,7 +543,7 @@
         getInstrumentation().waitForIdleSync();
 
         verify(mMockIDumpstate, times(1)).startBugreport(anyInt(), any(), any(), any(),
-                anyInt(), anyInt(), any(), anyBoolean());
+                anyInt(), anyInt(), any(), anyBoolean(), anyBoolean());
         sendBugreportFinished();
     }
 
@@ -608,7 +608,7 @@
         ArgumentCaptor<IDumpstateListener> listenerCap = ArgumentCaptor.forClass(
                 IDumpstateListener.class);
         verify(mMockIDumpstate, timeout(TIMEOUT)).startBugreport(anyInt(), any(), any(), any(),
-                anyInt(), anyInt(), listenerCap.capture(), anyBoolean());
+                anyInt(), anyInt(), listenerCap.capture(), anyBoolean(), anyBoolean());
         mIDumpstateListener = listenerCap.getValue();
         assertNotNull("Dumpstate listener should not be null", mIDumpstateListener);
         mIDumpstateListener.onProgress(0);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 8b60ed0..c4929a1 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -766,6 +766,7 @@
     ],
     static_libs: [
         "RoboTestLibraries",
+        "mockito-kotlin2",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index d6ca320..a45cd3d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -869,10 +869,6 @@
         mMetricLogger.logInteractionUnmute(device);
     }
 
-    String getPackageName() {
-        return mPackageName;
-    }
-
     boolean hasAdjustVolumeUserRestriction() {
         if (RestrictedLockUtilsInternal.checkIfRestrictionEnforced(
                 mContext, UserManager.DISALLOW_ADJUST_VOLUME, UserHandle.myUserId()) != null) {
@@ -1060,7 +1056,7 @@
     boolean isBroadcastSupported() {
         LocalBluetoothLeBroadcast broadcast =
                 mLocalBluetoothManager.getProfileManager().getLeAudioBroadcastProfile();
-        return broadcast != null ? true : false;
+        return broadcast != null;
     }
 
     boolean isBluetoothLeBroadcastEnabled() {
@@ -1194,13 +1190,6 @@
         assistant.unregisterServiceCallBack(callback);
     }
 
-    private boolean isPlayBackInfoLocal() {
-        return mMediaController != null
-                && mMediaController.getPlaybackInfo() != null
-                && mMediaController.getPlaybackInfo().getPlaybackType()
-                == MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL;
-    }
-
     boolean isPlaying() {
         if (mMediaController == null) {
             return false;
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java
deleted file mode 100644
index aeed78a..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.java
+++ /dev/null
@@ -1,432 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.settings;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.ContentResolver;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.provider.Settings;
-
-/**
- * Used to interact with mainly with Settings.Global, but can also be used for Settings.System
- * and Settings.Secure. To use the per-user System and Secure settings, {@link UserSettingsProxy}
- * must be used instead.
- * <p>
- * This interface can be implemented to give instance method (instead of static method) versions
- * of Settings.Global. It can be injected into class constructors and then faked or mocked as needed
- * in tests.
- * <p>
- * You can ask for {@link GlobalSettings} to be injected as needed.
- * <p>
- * This class also provides {@link #registerContentObserver(String, ContentObserver)} methods,
- * normally found on {@link ContentResolver} instances, unifying setting related actions in one
- * place.
- */
-public interface SettingsProxy {
-
-    /**
-     * Returns the {@link ContentResolver} this instance was constructed with.
-     */
-    ContentResolver getContentResolver();
-
-    /**
-     * Construct the content URI for a particular name/value pair,
-     * useful for monitoring changes with a ContentObserver.
-     * @param name to look up in the table
-     * @return the corresponding content URI, or null if not present
-     */
-    Uri getUriFor(String name);
-
-    /**
-     * Convenience wrapper around
-     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
-     * <p>
-     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
-     */
-    default void registerContentObserver(String name, ContentObserver settingsObserver) {
-        registerContentObserver(getUriFor(name), settingsObserver);
-    }
-
-    /**
-     * Convenience wrapper around
-     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
-     */
-    default void registerContentObserver(Uri uri, ContentObserver settingsObserver) {
-        registerContentObserver(uri, false, settingsObserver);
-    }
-
-    /**
-     * Convenience wrapper around
-     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
-     * <p>
-     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
-     */
-    default void registerContentObserver(String name, boolean notifyForDescendants,
-            ContentObserver settingsObserver) {
-        registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver);
-    }
-
-    /**
-     * Convenience wrapper around
-     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
-     */
-    default void registerContentObserver(Uri uri, boolean notifyForDescendants,
-            ContentObserver settingsObserver) {
-        getContentResolver().registerContentObserver(
-                uri, notifyForDescendants, settingsObserver);
-    }
-
-    /** See {@link ContentResolver#unregisterContentObserver(ContentObserver)}. */
-    default void unregisterContentObserver(ContentObserver settingsObserver) {
-        getContentResolver().unregisterContentObserver(settingsObserver);
-    }
-
-    /**
-     * Look up a name in the database.
-     * @param name to look up in the table
-     * @return the corresponding value, or null if not present
-     */
-    @Nullable
-    String getString(String name);
-
-    /**
-     * Store a name/value pair into the database.
-     * @param name to store
-     * @param value to associate with the name
-     * @return true if the value was set, false on database errors
-     */
-    boolean putString(String name, String value);
-
-    /**
-     * Store a name/value pair into the database.
-     * <p>
-     * The method takes an optional tag to associate with the setting
-     * which can be used to clear only settings made by your package and
-     * associated with this tag by passing the tag to {@link
-     * #resetToDefaults(String)}. Anyone can override
-     * the current tag. Also if another package changes the setting
-     * then the tag will be set to the one specified in the set call
-     * which can be null. Also any of the settings setters that do not
-     * take a tag as an argument effectively clears the tag.
-     * </p><p>
-     * For example, if you set settings A and B with tags T1 and T2 and
-     * another app changes setting A (potentially to the same value), it
-     * can assign to it a tag T3 (note that now the package that changed
-     * the setting is not yours). Now if you reset your changes for T1 and
-     * T2 only setting B will be reset and A not (as it was changed by
-     * another package) but since A did not change you are in the desired
-     * initial state. Now if the other app changes the value of A (assuming
-     * you registered an observer in the beginning) you would detect that
-     * the setting was changed by another app and handle this appropriately
-     * (ignore, set back to some value, etc).
-     * </p><p>
-     * Also the method takes an argument whether to make the value the
-     * default for this setting. If the system already specified a default
-     * value, then the one passed in here will <strong>not</strong>
-     * be set as the default.
-     * </p>
-     *
-     * @param name to store.
-     * @param value to associate with the name.
-     * @param tag to associate with the setting.
-     * @param makeDefault whether to make the value the default one.
-     * @return true if the value was set, false on database errors.
-     *
-     * @see #resetToDefaults(String)
-     *
-     */
-    boolean putString(@NonNull String name, @Nullable String value, @Nullable String tag,
-            boolean makeDefault);
-
-    /**
-     * Convenience function for retrieving a single secure settings value
-     * as an integer.  Note that internally setting values are always
-     * stored as strings; this function converts the string to an integer
-     * for you.  The default value will be returned if the setting is
-     * not defined or not an integer.
-     *
-     * @param name The name of the setting to retrieve.
-     * @param def Value to return if the setting is not defined.
-     *
-     * @return The setting's current value, or 'def' if it is not defined
-     * or not a valid integer.
-     */
-    default int getInt(String name, int def) {
-        String v = getString(name);
-        try {
-            return v != null ? Integer.parseInt(v) : def;
-        } catch (NumberFormatException e) {
-            return def;
-        }
-    }
-
-    /**
-     * Convenience function for retrieving a single secure settings value
-     * as an integer.  Note that internally setting values are always
-     * stored as strings; this function converts the string to an integer
-     * for you.
-     * <p>
-     * This version does not take a default value.  If the setting has not
-     * been set, or the string value is not a number,
-     * it throws {@link Settings.SettingNotFoundException}.
-     *
-     * @param name The name of the setting to retrieve.
-     *
-     * @throws Settings.SettingNotFoundException Thrown if a setting by the given
-     * name can't be found or the setting value is not an integer.
-     *
-     * @return The setting's current value.
-     */
-    default int getInt(String name)
-            throws Settings.SettingNotFoundException {
-        String v = getString(name);
-        try {
-            return Integer.parseInt(v);
-        } catch (NumberFormatException e) {
-            throw new Settings.SettingNotFoundException(name);
-        }
-    }
-
-    /**
-     * Convenience function for updating a single settings value as an
-     * integer. This will either create a new entry in the table if the
-     * given name does not exist, or modify the value of the existing row
-     * with that name.  Note that internally setting values are always
-     * stored as strings, so this function converts the given value to a
-     * string before storing it.
-     *
-     * @param name The name of the setting to modify.
-     * @param value The new value for the setting.
-     * @return true if the value was set, false on database errors
-     */
-    default boolean putInt(String name, int value) {
-        return putString(name, Integer.toString(value));
-    }
-
-    /**
-     * Convenience function for retrieving a single secure settings value
-     * as a boolean.  Note that internally setting values are always
-     * stored as strings; this function converts the string to a boolean
-     * for you.  The default value will be returned if the setting is
-     * not defined or not a boolean.
-     *
-     * @param name The name of the setting to retrieve.
-     * @param def Value to return if the setting is not defined.
-     *
-     * @return The setting's current value, or 'def' if it is not defined
-     * or not a valid boolean.
-     */
-    default boolean getBool(String name, boolean def) {
-        return getInt(name, def ? 1 : 0) != 0;
-    }
-
-    /**
-     * Convenience function for retrieving a single secure settings value
-     * as a boolean.  Note that internally setting values are always
-     * stored as strings; this function converts the string to a boolean
-     * for you.
-     * <p>
-     * This version does not take a default value.  If the setting has not
-     * been set, or the string value is not a number,
-     * it throws {@link Settings.SettingNotFoundException}.
-     *
-     * @param name The name of the setting to retrieve.
-     *
-     * @throws Settings.SettingNotFoundException Thrown if a setting by the given
-     * name can't be found or the setting value is not a boolean.
-     *
-     * @return The setting's current value.
-     */
-    default boolean getBool(String name)
-            throws Settings.SettingNotFoundException {
-        return getInt(name) != 0;
-    }
-
-    /**
-     * Convenience function for updating a single settings value as a
-     * boolean. This will either create a new entry in the table if the
-     * given name does not exist, or modify the value of the existing row
-     * with that name.  Note that internally setting values are always
-     * stored as strings, so this function converts the given value to a
-     * string before storing it.
-     *
-     * @param name The name of the setting to modify.
-     * @param value The new value for the setting.
-     * @return true if the value was set, false on database errors
-     */
-    default boolean putBool(String name, boolean value) {
-        return putInt(name, value ? 1 : 0);
-    }
-
-    /**
-     * Convenience function for retrieving a single secure settings value
-     * as a {@code long}.  Note that internally setting values are always
-     * stored as strings; this function converts the string to a {@code long}
-     * for you.  The default value will be returned if the setting is
-     * not defined or not a {@code long}.
-     *
-     * @param name The name of the setting to retrieve.
-     * @param def Value to return if the setting is not defined.
-     *
-     * @return The setting's current value, or 'def' if it is not defined
-     * or not a valid {@code long}.
-     */
-    default long getLong(String name, long def) {
-        String valString = getString(name);
-        return parseLongOrUseDefault(valString, def);
-    }
-
-    /** Convert a string to a long, or uses a default if the string is malformed or null */
-    static long parseLongOrUseDefault(String valString, long def) {
-        long value;
-        try {
-            value = valString != null ? Long.parseLong(valString) : def;
-        } catch (NumberFormatException e) {
-            value = def;
-        }
-        return value;
-    }
-
-    /**
-     * Convenience function for retrieving a single secure settings value
-     * as a {@code long}.  Note that internally setting values are always
-     * stored as strings; this function converts the string to a {@code long}
-     * for you.
-     * <p>
-     * This version does not take a default value.  If the setting has not
-     * been set, or the string value is not a number,
-     * it throws {@link Settings.SettingNotFoundException}.
-     *
-     * @param name The name of the setting to retrieve.
-     *
-     * @return The setting's current value.
-     * @throws Settings.SettingNotFoundException Thrown if a setting by the given
-     * name can't be found or the setting value is not an integer.
-     */
-    default long getLong(String name)
-            throws Settings.SettingNotFoundException {
-        String valString = getString(name);
-        return parseLongOrThrow(name, valString);
-    }
-
-    /** Convert a string to a long, or throws an exception if the string is malformed or null */
-    static long parseLongOrThrow(String name, String valString)
-            throws Settings.SettingNotFoundException {
-        try {
-            return Long.parseLong(valString);
-        } catch (NumberFormatException e) {
-            throw new Settings.SettingNotFoundException(name);
-        }
-    }
-
-    /**
-     * Convenience function for updating a secure settings value as a long
-     * integer. This will either create a new entry in the table if the
-     * given name does not exist, or modify the value of the existing row
-     * with that name.  Note that internally setting values are always
-     * stored as strings, so this function converts the given value to a
-     * string before storing it.
-     *
-     * @param name The name of the setting to modify.
-     * @param value The new value for the setting.
-     * @return true if the value was set, false on database errors
-     */
-    default boolean putLong(String name, long value) {
-        return putString(name, Long.toString(value));
-    }
-
-    /**
-     * Convenience function for retrieving a single secure settings value
-     * as a floating point number.  Note that internally setting values are
-     * always stored as strings; this function converts the string to an
-     * float for you. The default value will be returned if the setting
-     * is not defined or not a valid float.
-     *
-     * @param name The name of the setting to retrieve.
-     * @param def Value to return if the setting is not defined.
-     *
-     * @return The setting's current value, or 'def' if it is not defined
-     * or not a valid float.
-     */
-    default float getFloat(String name, float def) {
-        String v = getString(name);
-        return parseFloat(v, def);
-    }
-
-    /** Convert a string to a float, or uses a default if the string is malformed or null */
-    static float parseFloat(String v, float def) {
-        try {
-            return v != null ? Float.parseFloat(v) : def;
-        } catch (NumberFormatException e) {
-            return def;
-        }
-    }
-
-    /**
-     * Convenience function for retrieving a single secure settings value
-     * as a float.  Note that internally setting values are always
-     * stored as strings; this function converts the string to a float
-     * for you.
-     * <p>
-     * This version does not take a default value.  If the setting has not
-     * been set, or the string value is not a number,
-     * it throws {@link Settings.SettingNotFoundException}.
-     *
-     * @param name The name of the setting to retrieve.
-     *
-     * @throws Settings.SettingNotFoundException Thrown if a setting by the given
-     * name can't be found or the setting value is not a float.
-     *
-     * @return The setting's current value.
-     */
-    default float getFloat(String name)
-            throws Settings.SettingNotFoundException {
-        String v = getString(name);
-        return parseFloatOrThrow(name, v);
-    }
-
-    /** Convert a string to a float, or throws an exception if the string is malformed or null */
-    static float parseFloatOrThrow(String name, String v)
-            throws Settings.SettingNotFoundException {
-        if (v == null) {
-            throw new Settings.SettingNotFoundException(name);
-        }
-        try {
-            return Float.parseFloat(v);
-        } catch (NumberFormatException e) {
-            throw new Settings.SettingNotFoundException(name);
-        }
-    }
-
-    /**
-     * Convenience function for updating a single settings value as a
-     * floating point number. This will either create a new entry in the
-     * table if the given name does not exist, or modify the value of the
-     * existing row with that name.  Note that internally setting values
-     * are always stored as strings, so this function converts the given
-     * value to a string before storing it.
-     *
-     * @param name The name of the setting to modify.
-     * @param value The new value for the setting.
-     * @return true if the value was set, false on database errors
-     */
-    default boolean putFloat(String name, float value) {
-        return putString(name, Float.toString(value));
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
new file mode 100644
index 0000000..ec89610
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.util.settings
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.provider.Settings.SettingNotFoundException
+
+/**
+ * Used to interact with mainly with Settings.Global, but can also be used for Settings.System and
+ * Settings.Secure. To use the per-user System and Secure settings, [UserSettingsProxy] must be used
+ * instead.
+ *
+ * This interface can be implemented to give instance method (instead of static method) versions of
+ * Settings.Global. It can be injected into class constructors and then faked or mocked as needed in
+ * tests.
+ *
+ * You can ask for [GlobalSettings] to be injected as needed.
+ *
+ * This class also provides [.registerContentObserver] methods, normally found on [ContentResolver]
+ * instances, unifying setting related actions in one place.
+ */
+interface SettingsProxy {
+    /** Returns the [ContentResolver] this instance was constructed with. */
+    fun getContentResolver(): ContentResolver
+
+    /**
+     * Construct the content URI for a particular name/value pair, useful for monitoring changes
+     * with a ContentObserver.
+     *
+     * @param name to look up in the table
+     * @return the corresponding content URI, or null if not present
+     */
+    fun getUriFor(name: String): Uri
+
+    /**
+     * Convenience wrapper around [ContentResolver.registerContentObserver].'
+     *
+     * Implicitly calls [getUriFor] on the passed in name.
+     */
+    fun registerContentObserver(name: String, settingsObserver: ContentObserver) {
+        registerContentObserver(getUriFor(name), settingsObserver)
+    }
+
+    /** Convenience wrapper around [ContentResolver.registerContentObserver].' */
+    fun registerContentObserver(uri: Uri, settingsObserver: ContentObserver) =
+        registerContentObserver(uri, false, settingsObserver)
+
+    /**
+     * Convenience wrapper around [ContentResolver.registerContentObserver].'
+     *
+     * Implicitly calls [getUriFor] on the passed in name.
+     */
+    fun registerContentObserver(
+        name: String,
+        notifyForDescendants: Boolean,
+        settingsObserver: ContentObserver
+    ) = registerContentObserver(getUriFor(name), notifyForDescendants, settingsObserver)
+
+    /** Convenience wrapper around [ContentResolver.registerContentObserver].' */
+    fun registerContentObserver(
+        uri: Uri,
+        notifyForDescendants: Boolean,
+        settingsObserver: ContentObserver
+    ) = getContentResolver().registerContentObserver(uri, notifyForDescendants, settingsObserver)
+
+    /** See [ContentResolver.unregisterContentObserver]. */
+    fun unregisterContentObserver(settingsObserver: ContentObserver) =
+        getContentResolver().unregisterContentObserver(settingsObserver)
+
+    /**
+     * Look up a name in the database.
+     *
+     * @param name to look up in the table
+     * @return the corresponding value, or null if not present
+     */
+    fun getString(name: String): String
+
+    /**
+     * Store a name/value pair into the database.
+     *
+     * @param name to store
+     * @param value to associate with the name
+     * @return true if the value was set, false on database errors
+     */
+    fun putString(name: String, value: String): Boolean
+
+    /**
+     * Store a name/value pair into the database.
+     *
+     * The method takes an optional tag to associate with the setting which can be used to clear
+     * only settings made by your package and associated with this tag by passing the tag to
+     * [ ][.resetToDefaults]. Anyone can override the current tag. Also if another package changes
+     * the setting then the tag will be set to the one specified in the set call which can be null.
+     * Also any of the settings setters that do not take a tag as an argument effectively clears the
+     * tag.
+     *
+     * For example, if you set settings A and B with tags T1 and T2 and another app changes setting
+     * A (potentially to the same value), it can assign to it a tag T3 (note that now the package
+     * that changed the setting is not yours). Now if you reset your changes for T1 and T2 only
+     * setting B will be reset and A not (as it was changed by another package) but since A did not
+     * change you are in the desired initial state. Now if the other app changes the value of A
+     * (assuming you registered an observer in the beginning) you would detect that the setting was
+     * changed by another app and handle this appropriately (ignore, set back to some value, etc).
+     *
+     * Also the method takes an argument whether to make the value the default for this setting. If
+     * the system already specified a default value, then the one passed in here will **not** be set
+     * as the default.
+     *
+     * @param name to store.
+     * @param value to associate with the name.
+     * @param tag to associate with the setting.
+     * @param makeDefault whether to make the value the default one.
+     * @return true if the value was set, false on database errors.
+     * @see .resetToDefaults
+     */
+    fun putString(name: String, value: String, tag: String, makeDefault: Boolean): Boolean
+
+    /**
+     * Convenience function for retrieving a single secure settings value as an integer. Note that
+     * internally setting values are always stored as strings; this function converts the string to
+     * an integer for you. The default value will be returned if the setting is not defined or not
+     * an integer.
+     *
+     * @param name The name of the setting to retrieve.
+     * @param def Value to return if the setting is not defined.
+     * @return The setting's current value, or 'def' if it is not defined or not a valid integer.
+     */
+    fun getInt(name: String, def: Int): Int {
+        val v = getString(name)
+        return try {
+            v.toInt()
+        } catch (e: NumberFormatException) {
+            def
+        }
+    }
+
+    /**
+     * Convenience function for retrieving a single secure settings value as an integer. Note that
+     * internally setting values are always stored as strings; this function converts the string to
+     * an integer for you.
+     *
+     * This version does not take a default value. If the setting has not been set, or the string
+     * value is not a number, it throws [Settings.SettingNotFoundException].
+     *
+     * @param name The name of the setting to retrieve.
+     * @return The setting's current value.
+     * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be
+     *   found or the setting value is not an integer.
+     */
+    @Throws(SettingNotFoundException::class)
+    fun getInt(name: String): Int {
+        val v = getString(name)
+        return try {
+            v.toInt()
+        } catch (e: NumberFormatException) {
+            throw SettingNotFoundException(name)
+        }
+    }
+
+    /**
+     * Convenience function for updating a single settings value as an integer. This will either
+     * create a new entry in the table if the given name does not exist, or modify the value of the
+     * existing row with that name. Note that internally setting values are always stored as
+     * strings, so this function converts the given value to a string before storing it.
+     *
+     * @param name The name of the setting to modify.
+     * @param value The new value for the setting.
+     * @return true if the value was set, false on database errors
+     */
+    fun putInt(name: String, value: Int): Boolean {
+        return putString(name, value.toString())
+    }
+
+    /**
+     * Convenience function for retrieving a single secure settings value as a boolean. Note that
+     * internally setting values are always stored as strings; this function converts the string to
+     * a boolean for you. The default value will be returned if the setting is not defined or not a
+     * boolean.
+     *
+     * @param name The name of the setting to retrieve.
+     * @param def Value to return if the setting is not defined.
+     * @return The setting's current value, or 'def' if it is not defined or not a valid boolean.
+     */
+    fun getBool(name: String, def: Boolean): Boolean {
+        return getInt(name, if (def) 1 else 0) != 0
+    }
+
+    /**
+     * Convenience function for retrieving a single secure settings value as a boolean. Note that
+     * internally setting values are always stored as strings; this function converts the string to
+     * a boolean for you.
+     *
+     * This version does not take a default value. If the setting has not been set, or the string
+     * value is not a number, it throws [Settings.SettingNotFoundException].
+     *
+     * @param name The name of the setting to retrieve.
+     * @return The setting's current value.
+     * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be
+     *   found or the setting value is not a boolean.
+     */
+    @Throws(SettingNotFoundException::class)
+    fun getBool(name: String): Boolean {
+        return getInt(name) != 0
+    }
+
+    /**
+     * Convenience function for updating a single settings value as a boolean. This will either
+     * create a new entry in the table if the given name does not exist, or modify the value of the
+     * existing row with that name. Note that internally setting values are always stored as
+     * strings, so this function converts the given value to a string before storing it.
+     *
+     * @param name The name of the setting to modify.
+     * @param value The new value for the setting.
+     * @return true if the value was set, false on database errors
+     */
+    fun putBool(name: String, value: Boolean): Boolean {
+        return putInt(name, if (value) 1 else 0)
+    }
+
+    /**
+     * Convenience function for retrieving a single secure settings value as a `long`. Note that
+     * internally setting values are always stored as strings; this function converts the string to
+     * a `long` for you. The default value will be returned if the setting is not defined or not a
+     * `long`.
+     *
+     * @param name The name of the setting to retrieve.
+     * @param def Value to return if the setting is not defined.
+     * @return The setting's current value, or 'def' if it is not defined or not a valid `long`.
+     */
+    fun getLong(name: String, def: Long): Long {
+        val valString = getString(name)
+        return parseLongOrUseDefault(valString, def)
+    }
+
+    /**
+     * Convenience function for retrieving a single secure settings value as a `long`. Note that
+     * internally setting values are always stored as strings; this function converts the string to
+     * a `long` for you.
+     *
+     * This version does not take a default value. If the setting has not been set, or the string
+     * value is not a number, it throws [Settings.SettingNotFoundException].
+     *
+     * @param name The name of the setting to retrieve.
+     * @return The setting's current value.
+     * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be
+     *   found or the setting value is not an integer.
+     */
+    @Throws(SettingNotFoundException::class)
+    fun getLong(name: String): Long {
+        val valString = getString(name)
+        return parseLongOrThrow(name, valString)
+    }
+
+    /**
+     * Convenience function for updating a secure settings value as a long integer. This will either
+     * create a new entry in the table if the given name does not exist, or modify the value of the
+     * existing row with that name. Note that internally setting values are always stored as
+     * strings, so this function converts the given value to a string before storing it.
+     *
+     * @param name The name of the setting to modify.
+     * @param value The new value for the setting.
+     * @return true if the value was set, false on database errors
+     */
+    fun putLong(name: String, value: Long): Boolean {
+        return putString(name, value.toString())
+    }
+
+    /**
+     * Convenience function for retrieving a single secure settings value as a floating point
+     * number. Note that internally setting values are always stored as strings; this function
+     * converts the string to an float for you. The default value will be returned if the setting is
+     * not defined or not a valid float.
+     *
+     * @param name The name of the setting to retrieve.
+     * @param def Value to return if the setting is not defined.
+     * @return The setting's current value, or 'def' if it is not defined or not a valid float.
+     */
+    fun getFloat(name: String, def: Float): Float {
+        val v = getString(name)
+        return parseFloat(v, def)
+    }
+
+    /**
+     * Convenience function for retrieving a single secure settings value as a float. Note that
+     * internally setting values are always stored as strings; this function converts the string to
+     * a float for you.
+     *
+     * This version does not take a default value. If the setting has not been set, or the string
+     * value is not a number, it throws [Settings.SettingNotFoundException].
+     *
+     * @param name The name of the setting to retrieve.
+     * @return The setting's current value.
+     * @throws Settings.SettingNotFoundException Thrown if a setting by the given name can't be
+     *   found or the setting value is not a float.
+     */
+    @Throws(SettingNotFoundException::class)
+    fun getFloat(name: String): Float {
+        val v = getString(name)
+        return parseFloatOrThrow(name, v)
+    }
+
+    /**
+     * Convenience function for updating a single settings value as a floating point number. This
+     * will either create a new entry in the table if the given name does not exist, or modify the
+     * value of the existing row with that name. Note that internally setting values are always
+     * stored as strings, so this function converts the given value to a string before storing it.
+     *
+     * @param name The name of the setting to modify.
+     * @param value The new value for the setting.
+     * @return true if the value was set, false on database errors
+     */
+    fun putFloat(name: String, value: Float): Boolean {
+        return putString(name, value.toString())
+    }
+
+    companion object {
+        /** Convert a string to a long, or uses a default if the string is malformed or null */
+        @JvmStatic
+        fun parseLongOrUseDefault(valString: String, def: Long): Long {
+            val value: Long
+            value =
+                try {
+                    valString.toLong()
+                } catch (e: NumberFormatException) {
+                    def
+                }
+            return value
+        }
+
+        /** Convert a string to a long, or throws an exception if the string is malformed or null */
+        @JvmStatic
+        @Throws(SettingNotFoundException::class)
+        fun parseLongOrThrow(name: String, valString: String?): Long {
+            if (valString == null) {
+                throw SettingNotFoundException(name)
+            }
+            return try {
+                valString.toLong()
+            } catch (e: NumberFormatException) {
+                throw SettingNotFoundException(name)
+            }
+        }
+
+        /** Convert a string to a float, or uses a default if the string is malformed or null */
+        @JvmStatic
+        fun parseFloat(v: String?, def: Float): Float {
+            return try {
+                v?.toFloat() ?: def
+            } catch (e: NumberFormatException) {
+                def
+            }
+        }
+
+        /**
+         * Convert a string to a float, or throws an exception if the string is malformed or null
+         */
+        @JvmStatic
+        @Throws(SettingNotFoundException::class)
+        fun parseFloatOrThrow(name: String, v: String?): Float {
+            if (v == null) {
+                throw SettingNotFoundException(name)
+            }
+            return try {
+                v.toFloat()
+            } catch (e: NumberFormatException) {
+                throw SettingNotFoundException(name)
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java
deleted file mode 100644
index 10cf082..0000000
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.java
+++ /dev/null
@@ -1,283 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.util.settings;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.content.ContentResolver;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.UserHandle;
-import android.provider.Settings;
-
-import com.android.app.tracing.TraceUtils;
-import com.android.systemui.settings.UserTracker;
-
-import kotlin.Unit;
-
-/**
- * Used to interact with per-user Settings.Secure and Settings.System settings (but not
- * Settings.Global, since those do not vary per-user)
- * <p>
- * This interface can be implemented to give instance method (instead of static method) versions
- * of Settings.Secure and Settings.System. It can be injected into class constructors and then
- * faked or mocked as needed in tests.
- * <p>
- * You can ask for {@link SecureSettings} or {@link SystemSettings} to be injected as needed.
- * <p>
- * This class also provides {@link #registerContentObserver(String, ContentObserver)} methods,
- * normally found on {@link ContentResolver} instances, unifying setting related actions in one
- * place.
- */
-public interface UserSettingsProxy extends SettingsProxy {
-
-    /**
-     * Returns that {@link UserTracker} this instance was constructed with.
-     */
-    UserTracker getUserTracker();
-
-    /**
-     * Returns the user id for the associated {@link ContentResolver}.
-     */
-    default int getUserId() {
-        return getContentResolver().getUserId();
-    }
-
-    /**
-     * Returns the actual current user handle when querying with the current user. Otherwise,
-     * returns the passed in user id.
-     */
-    default int getRealUserHandle(int userHandle) {
-        if (userHandle != UserHandle.USER_CURRENT) {
-            return userHandle;
-        }
-        return getUserTracker().getUserId();
-    }
-
-    @Override
-    default void registerContentObserver(Uri uri, ContentObserver settingsObserver) {
-        registerContentObserverForUser(uri, settingsObserver, getUserId());
-    }
-
-    /**
-     * Convenience wrapper around
-     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver)}.'
-     */
-    @Override
-    default void registerContentObserver(Uri uri, boolean notifyForDescendants,
-            ContentObserver settingsObserver) {
-        registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, getUserId());
-    }
-
-    /**
-     * Convenience wrapper around
-     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
-     *
-     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
-     */
-    default void registerContentObserverForUser(
-            String name, ContentObserver settingsObserver, int userHandle) {
-        registerContentObserverForUser(
-                getUriFor(name), settingsObserver, userHandle);
-    }
-
-    /**
-     * Convenience wrapper around
-     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
-     */
-    default void registerContentObserverForUser(
-            Uri uri, ContentObserver settingsObserver, int userHandle) {
-        registerContentObserverForUser(
-                uri, false, settingsObserver, userHandle);
-    }
-
-    /**
-     * Convenience wrapper around
-     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
-     *
-     * Implicitly calls {@link #getUriFor(String)} on the passed in name.
-     */
-    default void registerContentObserverForUser(
-            String name, boolean notifyForDescendants, ContentObserver settingsObserver,
-            int userHandle) {
-        registerContentObserverForUser(
-                getUriFor(name), notifyForDescendants, settingsObserver, userHandle);
-    }
-
-    /**
-     * Convenience wrapper around
-     * {@link ContentResolver#registerContentObserver(Uri, boolean, ContentObserver, int)}
-     */
-    default void registerContentObserverForUser(
-            Uri uri, boolean notifyForDescendants, ContentObserver settingsObserver,
-            int userHandle) {
-        TraceUtils.trace(
-                () -> {
-                    // The limit for trace tags length is 127 chars, which leaves us 90 for Uri.
-                    return "USP#registerObserver#[" + uri.toString() + "]";
-                }, () -> {
-                    getContentResolver().registerContentObserver(
-                            uri, notifyForDescendants, settingsObserver,
-                            getRealUserHandle(userHandle));
-                    return Unit.INSTANCE;
-                });
-    }
-
-    /**
-     * Look up a name in the database.
-     * @param name to look up in the table
-     * @return the corresponding value, or null if not present
-     */
-    @Override
-    default String getString(String name) {
-        return getStringForUser(name, getUserId());
-    }
-
-    /**See {@link #getString(String)}. */
-    String getStringForUser(String name, int userHandle);
-
-    /**
-     * Store a name/value pair into the database. Values written by this method will be
-     * overridden if a restore happens in the future.
-     *
-     * @param name to store
-     * @param value to associate with the name
-     * @return true if the value was set, false on database errors
-     */
-    boolean putString(String name, String value, boolean overrideableByRestore);
-
-    @Override
-    default boolean putString(String name, String value) {
-        return putStringForUser(name, value, getUserId());
-    }
-
-    /** See {@link #putString(String, String)}. */
-    boolean putStringForUser(String name, String value, int userHandle);
-
-    /** See {@link #putString(String, String)}. */
-    boolean putStringForUser(@NonNull String name, @Nullable String value, @Nullable String tag,
-            boolean makeDefault, @UserIdInt int userHandle, boolean overrideableByRestore);
-
-    @Override
-    default int getInt(String name, int def) {
-        return getIntForUser(name, def, getUserId());
-    }
-
-    /** See {@link #getInt(String, int)}. */
-    default int getIntForUser(String name, int def, int userHandle) {
-        String v = getStringForUser(name, userHandle);
-        try {
-            return v != null ? Integer.parseInt(v) : def;
-        } catch (NumberFormatException e) {
-            return def;
-        }
-    }
-
-    @Override
-    default int getInt(String name) throws Settings.SettingNotFoundException {
-        return getIntForUser(name, getUserId());
-    }
-
-    /** See {@link #getInt(String)}. */
-    default int getIntForUser(String name, int userHandle)
-            throws Settings.SettingNotFoundException {
-        String v = getStringForUser(name, userHandle);
-        try {
-            return Integer.parseInt(v);
-        } catch (NumberFormatException e) {
-            throw new Settings.SettingNotFoundException(name);
-        }
-    }
-
-    @Override
-    default boolean putInt(String name, int value) {
-        return putIntForUser(name, value, getUserId());
-    }
-
-    /** See {@link #putInt(String, int)}. */
-    default boolean putIntForUser(String name, int value, int userHandle) {
-        return putStringForUser(name, Integer.toString(value), userHandle);
-    }
-
-    @Override
-    default boolean getBool(String name, boolean def) {
-        return getBoolForUser(name, def, getUserId());
-    }
-
-    /** See {@link #getBool(String, boolean)}. */
-    default boolean getBoolForUser(String name, boolean def, int userHandle) {
-        return getIntForUser(name, def ? 1 : 0, userHandle) != 0;
-    }
-
-    @Override
-    default boolean getBool(String name) throws Settings.SettingNotFoundException {
-        return getBoolForUser(name, getUserId());
-    }
-
-    /** See {@link #getBool(String)}. */
-    default boolean getBoolForUser(String name, int userHandle)
-            throws Settings.SettingNotFoundException {
-        return getIntForUser(name, userHandle) != 0;
-    }
-
-    @Override
-    default boolean putBool(String name, boolean value) {
-        return putBoolForUser(name, value, getUserId());
-    }
-
-    /** See {@link #putBool(String, boolean)}. */
-    default boolean putBoolForUser(String name, boolean value, int userHandle) {
-        return putIntForUser(name, value ? 1 : 0, userHandle);
-    }
-
-    /** See {@link #getLong(String, long)}. */
-    default long getLongForUser(String name, long def, int userHandle) {
-        String valString = getStringForUser(name, userHandle);
-        return SettingsProxy.parseLongOrUseDefault(valString, def);
-    }
-
-    /** See {@link #getLong(String)}. */
-    default long getLongForUser(String name, int userHandle)
-            throws Settings.SettingNotFoundException {
-        String valString = getStringForUser(name, userHandle);
-        return SettingsProxy.parseLongOrThrow(name, valString);
-    }
-
-    /** See {@link #putLong(String, long)}. */
-    default boolean putLongForUser(String name, long value, int userHandle) {
-        return putStringForUser(name, Long.toString(value), userHandle);
-    }
-
-    /** See {@link #getFloat(String)}. */
-    default float getFloatForUser(String name, float def, int userHandle) {
-        String v = getStringForUser(name, userHandle);
-        return SettingsProxy.parseFloat(v, def);
-    }
-
-    /** See {@link #getFloat(String, float)}. */
-    default float getFloatForUser(String name, int userHandle)
-            throws Settings.SettingNotFoundException {
-        String v = getStringForUser(name, userHandle);
-        return SettingsProxy.parseFloatOrThrow(name, v);
-    }
-
-    /** See {@link #putFloat(String, float)} */
-    default boolean putFloatForUser(String name, float value, int userHandle) {
-        return putStringForUser(name, Float.toString(value), userHandle);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
new file mode 100644
index 0000000..2285270
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
@@ -0,0 +1,269 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.systemui.util.settings
+
+import android.annotation.UserIdInt
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.UserHandle
+import android.provider.Settings.SettingNotFoundException
+import com.android.app.tracing.TraceUtils.trace
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloat
+import com.android.systemui.util.settings.SettingsProxy.Companion.parseFloatOrThrow
+import com.android.systemui.util.settings.SettingsProxy.Companion.parseLongOrThrow
+import com.android.systemui.util.settings.SettingsProxy.Companion.parseLongOrUseDefault
+
+/**
+ * Used to interact with per-user Settings.Secure and Settings.System settings (but not
+ * Settings.Global, since those do not vary per-user)
+ *
+ * This interface can be implemented to give instance method (instead of static method) versions of
+ * Settings.Secure and Settings.System. It can be injected into class constructors and then faked or
+ * mocked as needed in tests.
+ *
+ * You can ask for [SecureSettings] or [SystemSettings] to be injected as needed.
+ *
+ * This class also provides [.registerContentObserver] methods, normally found on [ContentResolver]
+ * instances, unifying setting related actions in one place.
+ */
+interface UserSettingsProxy : SettingsProxy {
+
+    /** Returns that [UserTracker] this instance was constructed with. */
+    val userTracker: UserTracker
+
+    /** Returns the user id for the associated [ContentResolver]. */
+    var userId: Int
+        get() = getContentResolver().userId
+        set(_) {
+            throw UnsupportedOperationException(
+                "userId cannot be set in interface, use setter from an implementation instead."
+            )
+        }
+
+    /**
+     * Returns the actual current user handle when querying with the current user. Otherwise,
+     * returns the passed in user id.
+     */
+    fun getRealUserHandle(userHandle: Int): Int {
+        return if (userHandle != UserHandle.USER_CURRENT) {
+            userHandle
+        } else userTracker.userId
+    }
+
+    override fun registerContentObserver(uri: Uri, settingsObserver: ContentObserver) {
+        registerContentObserverForUser(uri, settingsObserver, userId)
+    }
+
+    /** Convenience wrapper around [ContentResolver.registerContentObserver].' */
+    override fun registerContentObserver(
+        uri: Uri,
+        notifyForDescendants: Boolean,
+        settingsObserver: ContentObserver
+    ) {
+        registerContentObserverForUser(uri, notifyForDescendants, settingsObserver, userId)
+    }
+
+    /**
+     * Convenience wrapper around [ContentResolver.registerContentObserver]
+     *
+     * Implicitly calls [getUriFor] on the passed in name.
+     */
+    fun registerContentObserverForUser(
+        name: String,
+        settingsObserver: ContentObserver,
+        userHandle: Int
+    ) {
+        registerContentObserverForUser(getUriFor(name), settingsObserver, userHandle)
+    }
+
+    /** Convenience wrapper around [ContentResolver.registerContentObserver] */
+    fun registerContentObserverForUser(
+        uri: Uri,
+        settingsObserver: ContentObserver,
+        userHandle: Int
+    ) {
+        registerContentObserverForUser(uri, false, settingsObserver, userHandle)
+    }
+
+    /**
+     * Convenience wrapper around [ContentResolver.registerContentObserver]
+     *
+     * Implicitly calls [getUriFor] on the passed in name.
+     */
+    fun registerContentObserverForUser(
+        name: String,
+        notifyForDescendants: Boolean,
+        settingsObserver: ContentObserver,
+        userHandle: Int
+    ) {
+        registerContentObserverForUser(
+            getUriFor(name),
+            notifyForDescendants,
+            settingsObserver,
+            userHandle
+        )
+    }
+
+    /** Convenience wrapper around [ContentResolver.registerContentObserver] */
+    fun registerContentObserverForUser(
+        uri: Uri,
+        notifyForDescendants: Boolean,
+        settingsObserver: ContentObserver,
+        userHandle: Int
+    ) {
+        trace({ "USP#registerObserver#[$uri]" }) {
+            getContentResolver()
+                .registerContentObserver(
+                    uri,
+                    notifyForDescendants,
+                    settingsObserver,
+                    getRealUserHandle(userHandle)
+                )
+            Unit
+        }
+    }
+
+    /**
+     * Look up a name in the database.
+     *
+     * @param name to look up in the table
+     * @return the corresponding value, or null if not present
+     */
+    override fun getString(name: String): String {
+        return getStringForUser(name, userId)
+    }
+
+    /** See [getString]. */
+    fun getStringForUser(name: String, userHandle: Int): String
+
+    /**
+     * Store a name/value pair into the database. Values written by this method will be overridden
+     * if a restore happens in the future.
+     *
+     * @param name to store
+     * @param value to associate with the name
+     * @return true if the value was set, false on database errors
+     */
+    fun putString(name: String, value: String, overrideableByRestore: Boolean): Boolean
+    override fun putString(name: String, value: String): Boolean {
+        return putStringForUser(name, value, userId)
+    }
+
+    /** Similar implementation to [putString] for the specified [userHandle]. */
+    fun putStringForUser(name: String, value: String, userHandle: Int): Boolean
+
+    /** Similar implementation to [putString] for the specified [userHandle]. */
+    fun putStringForUser(
+        name: String,
+        value: String,
+        tag: String?,
+        makeDefault: Boolean,
+        @UserIdInt userHandle: Int,
+        overrideableByRestore: Boolean
+    ): Boolean
+
+    override fun getInt(name: String, def: Int): Int {
+        return getIntForUser(name, def, userId)
+    }
+
+    /** Similar implementation to [getInt] for the specified [userHandle]. */
+    fun getIntForUser(name: String, def: Int, userHandle: Int): Int {
+        val v = getStringForUser(name, userHandle)
+        return try {
+            v.toInt()
+        } catch (e: NumberFormatException) {
+            def
+        }
+    }
+
+    @Throws(SettingNotFoundException::class)
+    override fun getInt(name: String) = getIntForUser(name, userId)
+
+    /** Similar implementation to [getInt] for the specified [userHandle]. */
+    @Throws(SettingNotFoundException::class)
+    fun getIntForUser(name: String, userHandle: Int): Int {
+        val v = getStringForUser(name, userHandle)
+        return try {
+            v.toInt()
+        } catch (e: NumberFormatException) {
+            throw SettingNotFoundException(name)
+        }
+    }
+
+    override fun putInt(name: String, value: Int) = putIntForUser(name, value, userId)
+
+    /** Similar implementation to [getInt] for the specified [userHandle]. */
+    fun putIntForUser(name: String, value: Int, userHandle: Int) =
+        putStringForUser(name, value.toString(), userHandle)
+
+    override fun getBool(name: String, def: Boolean) = getBoolForUser(name, def, userId)
+
+    /** Similar implementation to [getBool] for the specified [userHandle]. */
+    fun getBoolForUser(name: String, def: Boolean, userHandle: Int) =
+        getIntForUser(name, if (def) 1 else 0, userHandle) != 0
+
+    @Throws(SettingNotFoundException::class)
+    override fun getBool(name: String) = getBoolForUser(name, userId)
+
+    /** Similar implementation to [getBool] for the specified [userHandle]. */
+    @Throws(SettingNotFoundException::class)
+    fun getBoolForUser(name: String, userHandle: Int): Boolean {
+        return getIntForUser(name, userHandle) != 0
+    }
+
+    override fun putBool(name: String, value: Boolean): Boolean {
+        return putBoolForUser(name, value, userId)
+    }
+
+    /** Similar implementation to [putBool] for the specified [userHandle]. */
+    fun putBoolForUser(name: String, value: Boolean, userHandle: Int) =
+        putIntForUser(name, if (value) 1 else 0, userHandle)
+
+    /** Similar implementation to [getLong] for the specified [userHandle]. */
+    fun getLongForUser(name: String, def: Long, userHandle: Int): Long {
+        val valString = getStringForUser(name, userHandle)
+        return parseLongOrUseDefault(valString, def)
+    }
+
+    /** Similar implementation to [getLong] for the specified [userHandle]. */
+    @Throws(SettingNotFoundException::class)
+    fun getLongForUser(name: String, userHandle: Int): Long {
+        val valString = getStringForUser(name, userHandle)
+        return parseLongOrThrow(name, valString)
+    }
+
+    /** Similar implementation to [putLong] for the specified [userHandle]. */
+    fun putLongForUser(name: String, value: Long, userHandle: Int) =
+        putStringForUser(name, value.toString(), userHandle)
+
+    /** Similar implementation to [getFloat] for the specified [userHandle]. */
+    fun getFloatForUser(name: String, def: Float, userHandle: Int): Float {
+        val v = getStringForUser(name, userHandle)
+        return parseFloat(v, def)
+    }
+
+    /** Similar implementation to [getFloat] for the specified [userHandle]. */
+    @Throws(SettingNotFoundException::class)
+    fun getFloatForUser(name: String, userHandle: Int): Float {
+        val v = getStringForUser(name, userHandle)
+        return parseFloatOrThrow(name, v)
+    }
+
+    /** Similar implementation to [putFloat] for the specified [userHandle]. */
+    fun putFloatForUser(name: String, value: Float, userHandle: Int) =
+        putStringForUser(name, value.toString(), userHandle)
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt
index 0df4fbf..9ba56d2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/data/repository/FaceSettingsRepositoryImplTest.kt
@@ -36,12 +36,12 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.junit.runners.JUnit4
-import org.mockito.ArgumentMatchers.any
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
+import org.mockito.kotlin.any
 
 private const val USER_ID = 8
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
index 7856f9b..a89139b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerTest.kt
@@ -196,7 +196,7 @@
         verify(globalSettings)
             .registerContentObserver(
                 eq(Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE)),
-                settingsObserverCaptor.capture()
+                capture(settingsObserverCaptor)
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
new file mode 100644
index 0000000..ab95707
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.settings
+
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings.SettingNotFoundException
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.eq
+
+/** Tests for [SettingsProxy]. */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class SettingsProxyTest : SysuiTestCase() {
+
+    private lateinit var mSettings: SettingsProxy
+    private lateinit var mContentObserver: ContentObserver
+
+    @Before
+    fun setUp() {
+        mSettings = FakeSettingsProxy()
+        mContentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {}
+    }
+
+    @Test
+    fun registerContentObserver_inputString_success() {
+        mSettings.registerContentObserver(TEST_SETTING, mContentObserver)
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver))
+    }
+
+    @Test
+    fun registerContentObserver_inputString_notifyForDescendants_true() {
+        mSettings.registerContentObserver(
+            TEST_SETTING,
+            notifyForDescendants = true,
+            mContentObserver
+        )
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver))
+    }
+
+    @Test
+    fun registerContentObserver_inputUri_success() {
+        mSettings.registerContentObserver(TEST_SETTING_URI, mContentObserver)
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver))
+    }
+
+    @Test
+    fun registerContentObserver_inputUri_notifyForDescendants_true() {
+        mSettings.registerContentObserver(
+            TEST_SETTING_URI,
+            notifyForDescendants = true,
+            mContentObserver
+        )
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver))
+    }
+
+    @Test
+    fun unregisterContentObserver() {
+        mSettings.unregisterContentObserver(mContentObserver)
+        verify(mSettings.getContentResolver()).unregisterContentObserver(eq(mContentObserver))
+    }
+
+    @Test
+    fun getString_keyPresent_returnValidValue() {
+        mSettings.putString(TEST_SETTING, "test")
+        assertThat(mSettings.getString(TEST_SETTING)).isEqualTo("test")
+    }
+
+    @Test
+    fun getString_keyAbsent_returnEmptyValue() {
+        assertThat(mSettings.getString(TEST_SETTING)).isEmpty()
+    }
+
+    @Test
+    fun getInt_keyPresent_returnValidValue() {
+        mSettings.putInt(TEST_SETTING, 2)
+        assertThat(mSettings.getInt(TEST_SETTING)).isEqualTo(2)
+    }
+
+    @Test
+    fun getInt_keyPresent_nonIntegerValue_throwException() {
+        assertThrows(SettingNotFoundException::class.java) {
+            mSettings.putString(TEST_SETTING, "test")
+            mSettings.getInt(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getInt_keyAbsent_throwException() {
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getInt(TEST_SETTING) }
+    }
+
+    @Test
+    fun getInt_keyAbsent_returnDefaultValue() {
+        assertThat(mSettings.getInt(TEST_SETTING, 5)).isEqualTo(5)
+    }
+
+    @Test
+    fun getBool_keyPresent_returnValidValue() {
+        mSettings.putBool(TEST_SETTING, true)
+        assertThat(mSettings.getBool(TEST_SETTING)).isTrue()
+    }
+
+    @Test
+    fun getBool_keyPresent_nonBooleanValue_throwException() {
+        assertThrows(SettingNotFoundException::class.java) {
+            mSettings.putString(TEST_SETTING, "test")
+            mSettings.getBool(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getBool_keyAbsent_throwException() {
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getBool(TEST_SETTING) }
+    }
+
+    @Test
+    fun getBool_keyAbsent_returnDefaultValue() {
+        assertThat(mSettings.getBool(TEST_SETTING, false)).isEqualTo(false)
+    }
+
+    @Test
+    fun getLong_keyPresent_returnValidValue() {
+        mSettings.putLong(TEST_SETTING, 1L)
+        assertThat(mSettings.getLong(TEST_SETTING)).isEqualTo(1L)
+    }
+
+    @Test
+    fun getLong_keyPresent_nonLongValue_throwException() {
+        assertThrows(SettingNotFoundException::class.java) {
+            mSettings.putString(TEST_SETTING, "test")
+            mSettings.getLong(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getLong_keyAbsent_throwException() {
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getLong(TEST_SETTING) }
+    }
+
+    @Test
+    fun getLong_keyAbsent_returnDefaultValue() {
+        assertThat(mSettings.getLong(TEST_SETTING, 2L)).isEqualTo(2L)
+    }
+
+    @Test
+    fun getFloat_keyPresent_returnValidValue() {
+        mSettings.putFloat(TEST_SETTING, 2.5F)
+        assertThat(mSettings.getFloat(TEST_SETTING)).isEqualTo(2.5F)
+    }
+
+    @Test
+    fun getFloat_keyPresent_nonFloatValue_throwException() {
+        assertThrows(SettingNotFoundException::class.java) {
+            mSettings.putString(TEST_SETTING, "test")
+            mSettings.getFloat(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getFloat_keyAbsent_throwException() {
+        assertThrows(SettingNotFoundException::class.java) { mSettings.getFloat(TEST_SETTING) }
+    }
+
+    @Test
+    fun getFloat_keyAbsent_returnDefaultValue() {
+        assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F)
+    }
+
+    private class FakeSettingsProxy : SettingsProxy {
+
+        private val mContentResolver = mock(ContentResolver::class.java)
+        private val settingToValueMap: MutableMap<String, String> = mutableMapOf()
+
+        override fun getContentResolver() = mContentResolver
+
+        override fun getUriFor(name: String) =
+            Uri.parse(StringBuilder().append("content://settings/").append(name).toString())
+
+        override fun getString(name: String): String {
+            return settingToValueMap[name] ?: ""
+        }
+
+        override fun putString(name: String, value: String): Boolean {
+            settingToValueMap[name] = value
+            return true
+        }
+
+        override fun putString(
+            name: String,
+            value: String,
+            tag: String,
+            makeDefault: Boolean
+        ): Boolean {
+            settingToValueMap[name] = value
+            return true
+        }
+    }
+
+    companion object {
+        private const val TEST_SETTING = "test_setting"
+        private val TEST_SETTING_URI = Uri.parse("content://settings/test_setting")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
new file mode 100644
index 0000000..56328b9
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.settings
+
+import android.content.ContentResolver
+import android.content.pm.UserInfo
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.settings.FakeUserTracker
+import com.android.systemui.settings.UserTracker
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.eq
+
+/** Tests for [UserSettingsProxy]. */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class UserSettingsProxyTest : SysuiTestCase() {
+
+    private var mUserTracker = FakeUserTracker()
+    private var mSettings: UserSettingsProxy = FakeUserSettingsProxy(mUserTracker)
+    private var mContentObserver = object : ContentObserver(Handler(Looper.getMainLooper())) {}
+
+    @Before
+    fun setUp() {
+        mUserTracker.set(
+            listOf(UserInfo(MAIN_USER_ID, "main", UserInfo.FLAG_MAIN)),
+            selectedUserIndex = 0
+        )
+    }
+
+    @Test
+    fun registerContentObserverForUser_inputString_success() {
+        mSettings.registerContentObserverForUser(
+            TEST_SETTING,
+            mContentObserver,
+            mUserTracker.userId
+        )
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(
+                eq(TEST_SETTING_URI),
+                eq(false),
+                eq(mContentObserver),
+                eq(MAIN_USER_ID)
+            )
+    }
+
+    @Test
+    fun registerContentObserverForUser_inputString_notifyForDescendants_true() {
+        mSettings.registerContentObserverForUser(
+            TEST_SETTING,
+            notifyForDescendants = true,
+            mContentObserver,
+            mUserTracker.userId
+        )
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(
+                eq(TEST_SETTING_URI),
+                eq(true),
+                eq(mContentObserver),
+                eq(MAIN_USER_ID)
+            )
+    }
+
+    @Test
+    fun registerContentObserverForUser_inputUri_success() {
+        mSettings.registerContentObserverForUser(
+            TEST_SETTING_URI,
+            mContentObserver,
+            mUserTracker.userId
+        )
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(
+                eq(TEST_SETTING_URI),
+                eq(false),
+                eq(mContentObserver),
+                eq(MAIN_USER_ID)
+            )
+    }
+
+    @Test
+    fun registerContentObserverForUser_inputUri_notifyForDescendants_true() {
+        mSettings.registerContentObserverForUser(
+            TEST_SETTING_URI,
+            notifyForDescendants = true,
+            mContentObserver,
+            mUserTracker.userId
+        )
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(
+                eq(TEST_SETTING_URI),
+                eq(true),
+                eq(mContentObserver),
+                eq(MAIN_USER_ID)
+            )
+    }
+
+    @Test
+    fun registerContentObserver_inputUri_success() {
+        mSettings.registerContentObserver(TEST_SETTING_URI, mContentObserver)
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(eq(TEST_SETTING_URI), eq(false), eq(mContentObserver), eq(0))
+    }
+
+    @Test
+    fun registerContentObserver_inputUri_notifyForDescendants_true() {
+        mSettings.registerContentObserver(
+            TEST_SETTING_URI,
+            notifyForDescendants = true,
+            mContentObserver
+        )
+        verify(mSettings.getContentResolver())
+            .registerContentObserver(eq(TEST_SETTING_URI), eq(true), eq(mContentObserver), eq(0))
+    }
+
+    @Test
+    fun getString_keyPresent_returnValidValue() {
+        mSettings.putString(TEST_SETTING, "test")
+        assertThat(mSettings.getString(TEST_SETTING)).isEqualTo("test")
+    }
+
+    @Test
+    fun getString_keyAbsent_returnEmptyValue() {
+        assertThat(mSettings.getString(TEST_SETTING)).isEmpty()
+    }
+
+    @Test
+    fun getStringForUser_multipleUsers_validResult() {
+        mSettings.putStringForUser(TEST_SETTING, "test", MAIN_USER_ID)
+        mSettings.putStringForUser(TEST_SETTING, "test1", SECONDARY_USER_ID)
+        assertThat(mSettings.getStringForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo("test")
+        assertThat(mSettings.getStringForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo("test1")
+    }
+
+    @Test
+    fun getInt_keyPresent_returnValidValue() {
+        mSettings.putInt(TEST_SETTING, 2)
+        assertThat(mSettings.getInt(TEST_SETTING)).isEqualTo(2)
+    }
+
+    @Test
+    fun getInt_keyPresent_nonIntegerValue_throwException() {
+        assertThrows(Settings.SettingNotFoundException::class.java) {
+            mSettings.putString(TEST_SETTING, "test")
+            mSettings.getInt(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getInt_keyAbsent_throwException() {
+        assertThrows(Settings.SettingNotFoundException::class.java) {
+            mSettings.getInt(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getInt_keyAbsent_returnDefaultValue() {
+        assertThat(mSettings.getInt(TEST_SETTING, 5)).isEqualTo(5)
+    }
+
+    @Test
+    fun getIntForUser_multipleUsers__validResult() {
+        mSettings.putIntForUser(TEST_SETTING, 1, MAIN_USER_ID)
+        mSettings.putIntForUser(TEST_SETTING, 2, SECONDARY_USER_ID)
+        assertThat(mSettings.getIntForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo(1)
+        assertThat(mSettings.getIntForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo(2)
+    }
+
+    @Test
+    fun getBool_keyPresent_returnValidValue() {
+        mSettings.putBool(TEST_SETTING, true)
+        assertThat(mSettings.getBool(TEST_SETTING)).isTrue()
+    }
+
+    @Test
+    fun getBool_keyPresent_nonBooleanValue_throwException() {
+        assertThrows(Settings.SettingNotFoundException::class.java) {
+            mSettings.putString(TEST_SETTING, "test")
+            mSettings.getBool(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getBool_keyAbsent_throwException() {
+        assertThrows(Settings.SettingNotFoundException::class.java) {
+            mSettings.getBool(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getBool_keyAbsent_returnDefaultValue() {
+        assertThat(mSettings.getBool(TEST_SETTING, false)).isEqualTo(false)
+    }
+
+    @Test
+    fun getBoolForUser_multipleUsers__validResult() {
+        mSettings.putBoolForUser(TEST_SETTING, true, MAIN_USER_ID)
+        mSettings.putBoolForUser(TEST_SETTING, false, SECONDARY_USER_ID)
+        assertThat(mSettings.getBoolForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo(true)
+        assertThat(mSettings.getBoolForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo(false)
+    }
+
+    @Test
+    fun getLong_keyPresent_returnValidValue() {
+        mSettings.putLong(TEST_SETTING, 1L)
+        assertThat(mSettings.getLong(TEST_SETTING)).isEqualTo(1L)
+    }
+
+    @Test
+    fun getLong_keyPresent_nonLongValue_throwException() {
+        assertThrows(Settings.SettingNotFoundException::class.java) {
+            mSettings.putString(TEST_SETTING, "test")
+            mSettings.getLong(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getLong_keyAbsent_throwException() {
+        assertThrows(Settings.SettingNotFoundException::class.java) {
+            mSettings.getLong(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getLong_keyAbsent_returnDefaultValue() {
+        assertThat(mSettings.getLong(TEST_SETTING, 2L)).isEqualTo(2L)
+    }
+
+    @Test
+    fun getLongForUser_multipleUsers__validResult() {
+        mSettings.putLongForUser(TEST_SETTING, 1L, MAIN_USER_ID)
+        mSettings.putLongForUser(TEST_SETTING, 2L, SECONDARY_USER_ID)
+        assertThat(mSettings.getLongForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo(1L)
+        assertThat(mSettings.getLongForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo(2L)
+    }
+
+    @Test
+    fun getFloat_keyPresent_returnValidValue() {
+        mSettings.putFloat(TEST_SETTING, 2.5F)
+        assertThat(mSettings.getFloat(TEST_SETTING)).isEqualTo(2.5F)
+    }
+
+    @Test
+    fun getFloat_keyPresent_nonFloatValue_throwException() {
+        assertThrows(Settings.SettingNotFoundException::class.java) {
+            mSettings.putString(TEST_SETTING, "test")
+            mSettings.getFloat(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getFloat_keyAbsent_throwException() {
+        assertThrows(Settings.SettingNotFoundException::class.java) {
+            mSettings.getFloat(TEST_SETTING)
+        }
+    }
+
+    @Test
+    fun getFloat_keyAbsent_returnDefaultValue() {
+        assertThat(mSettings.getFloat(TEST_SETTING, 2.5F)).isEqualTo(2.5F)
+    }
+
+    @Test
+    fun getFloatForUser_multipleUsers__validResult() {
+        mSettings.putFloatForUser(TEST_SETTING, 1F, MAIN_USER_ID)
+        mSettings.putFloatForUser(TEST_SETTING, 2F, SECONDARY_USER_ID)
+        assertThat(mSettings.getFloatForUser(TEST_SETTING, MAIN_USER_ID)).isEqualTo(1F)
+        assertThat(mSettings.getFloatForUser(TEST_SETTING, SECONDARY_USER_ID)).isEqualTo(2F)
+    }
+
+    /**
+     * Fake implementation of [UserSettingsProxy].
+     *
+     * This class uses a mock of [ContentResolver] to test the [ContentObserver] registration APIs.
+     */
+    private class FakeUserSettingsProxy(override val userTracker: UserTracker) : UserSettingsProxy {
+
+        private val mContentResolver = mock(ContentResolver::class.java)
+        private val userIdToSettingsValueMap: MutableMap<Int, MutableMap<String, String>> =
+            mutableMapOf()
+
+        override fun getContentResolver() = mContentResolver
+
+        override fun getUriFor(name: String) =
+            Uri.parse(StringBuilder().append(URI_PREFIX).append(name).toString())
+
+        override fun getStringForUser(name: String, userHandle: Int) =
+            userIdToSettingsValueMap[userHandle]?.get(name) ?: ""
+
+        override fun putString(
+            name: String,
+            value: String,
+            overrideableByRestore: Boolean
+        ): Boolean {
+            userIdToSettingsValueMap[DEFAULT_USER_ID]?.put(name, value)
+            return true
+        }
+
+        override fun putString(
+            name: String,
+            value: String,
+            tag: String,
+            makeDefault: Boolean
+        ): Boolean {
+            putStringForUser(name, value, DEFAULT_USER_ID)
+            return true
+        }
+
+        override fun putStringForUser(name: String, value: String, userHandle: Int): Boolean {
+            userIdToSettingsValueMap[userHandle] = mutableMapOf(Pair(name, value))
+            return true
+        }
+
+        override fun putStringForUser(
+            name: String,
+            value: String,
+            tag: String?,
+            makeDefault: Boolean,
+            userHandle: Int,
+            overrideableByRestore: Boolean
+        ): Boolean {
+            userIdToSettingsValueMap[userHandle]?.set(name, value)
+            return true
+        }
+
+        private companion object {
+            const val DEFAULT_USER_ID = 0
+            const val URI_PREFIX = "content://settings/"
+        }
+    }
+
+    private companion object {
+        const val MAIN_USER_ID = 10
+        const val SECONDARY_USER_ID = 20
+        const val TEST_SETTING = "test_setting"
+        val TEST_SETTING_URI = Uri.parse("content://settings/test_setting")
+    }
+}
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 58855ea..4c8f416 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -5590,32 +5590,30 @@
             // security checking for it above.
             userId = UserHandle.USER_CURRENT;
         }
-        try {
-            if (owningUid != 0 && owningUid != SYSTEM_UID) {
-                final int uid = AppGlobals.getPackageManager().getPackageUid(packageName,
-                        MATCH_DEBUG_TRIAGED_MISSING, UserHandle.getUserId(owningUid));
-                if (!UserHandle.isSameApp(owningUid, uid)) {
-                    String msg = "Permission Denial: getIntentSender() from pid="
-                            + Binder.getCallingPid()
-                            + ", uid=" + owningUid
-                            + ", (need uid=" + uid + ")"
-                            + " is not allowed to send as package " + packageName;
-                    Slog.w(TAG, msg);
-                    throw new SecurityException(msg);
-                }
-            }
 
-            if (type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT) {
-                return mAtmInternal.getIntentSender(type, packageName, featureId, owningUid,
-                        userId, token, resultWho, requestCode, intents, resolvedTypes, flags,
-                        bOptions);
+        if (owningUid != 0 && owningUid != SYSTEM_UID) {
+            if (!getPackageManagerInternal().isSameApp(
+                    packageName,
+                    MATCH_DEBUG_TRIAGED_MISSING,
+                    owningUid,
+                    UserHandle.getUserId(owningUid))) {
+                String msg = "Permission Denial: getIntentSender() from pid="
+                        + Binder.getCallingPid()
+                        + ", uid=" + owningUid
+                        + " is not allowed to send as package " + packageName;
+                Slog.w(TAG, msg);
+                throw new SecurityException(msg);
             }
-            return mPendingIntentController.getIntentSender(type, packageName, featureId,
-                    owningUid, userId, token, resultWho, requestCode, intents, resolvedTypes,
-                    flags, bOptions);
-        } catch (RemoteException e) {
-            throw new SecurityException(e);
         }
+
+        if (type == ActivityManager.INTENT_SENDER_ACTIVITY_RESULT) {
+            return mAtmInternal.getIntentSender(type, packageName, featureId, owningUid,
+                    userId, token, resultWho, requestCode, intents, resolvedTypes, flags,
+                    bOptions);
+        }
+        return mPendingIntentController.getIntentSender(type, packageName, featureId,
+                owningUid, userId, token, resultWho, requestCode, intents, resolvedTypes,
+                flags, bOptions);
     }
 
     @Override
diff --git a/services/core/java/com/android/server/display/DisplayAdapter.java b/services/core/java/com/android/server/display/DisplayAdapter.java
index c26118e..5690a9e 100644
--- a/services/core/java/com/android/server/display/DisplayAdapter.java
+++ b/services/core/java/com/android/server/display/DisplayAdapter.java
@@ -135,7 +135,7 @@
             float[] alternativeRefreshRates,
             @Display.HdrCapabilities.HdrType int[] supportedHdrTypes) {
         return new Display.Mode(NEXT_DISPLAY_MODE_ID.getAndIncrement(), width, height, refreshRate,
-                vsyncRate, alternativeRefreshRates, supportedHdrTypes);
+                vsyncRate, false, alternativeRefreshRates, supportedHdrTypes);
     }
 
     public interface Listener {
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 189e366..5d55d190 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -35,6 +35,7 @@
 
 import com.android.server.display.layout.Layout;
 import com.android.server.display.mode.DisplayModeDirector;
+import com.android.server.display.mode.SyntheticModeManager;
 import com.android.server.wm.utils.InsetUtils;
 
 import java.io.PrintWriter;
@@ -408,7 +409,8 @@
      *
      * @param deviceRepo Repository of active {@link DisplayDevice}s.
      */
-    public void updateLocked(DisplayDeviceRepository deviceRepo) {
+    public void updateLocked(DisplayDeviceRepository deviceRepo,
+            SyntheticModeManager syntheticModeManager) {
         // Nothing to update if already invalid.
         if (mPrimaryDisplayDevice == null) {
             return;
@@ -426,6 +428,7 @@
         // logical display that they are sharing.  (eg. Adjust size for pixel-perfect
         // mirroring over HDMI.)
         DisplayDeviceInfo deviceInfo = mPrimaryDisplayDevice.getDisplayDeviceInfoLocked();
+        DisplayDeviceConfig config = mPrimaryDisplayDevice.getDisplayDeviceConfig();
         if (!Objects.equals(mPrimaryDisplayDeviceInfo, deviceInfo) || mDirty) {
             mBaseDisplayInfo.layerStack = mLayerStack;
             mBaseDisplayInfo.flags = 0;
@@ -507,6 +510,9 @@
             mBaseDisplayInfo.userPreferredModeId = deviceInfo.userPreferredModeId;
             mBaseDisplayInfo.supportedModes = Arrays.copyOf(
                     deviceInfo.supportedModes, deviceInfo.supportedModes.length);
+            mBaseDisplayInfo.appsSupportedModes = syntheticModeManager.createAppSupportedModes(
+                    config, mBaseDisplayInfo.supportedModes
+            );
             mBaseDisplayInfo.colorMode = deviceInfo.colorMode;
             mBaseDisplayInfo.supportedColorModes = Arrays.copyOf(
                     deviceInfo.supportedColorModes,
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index bca53cf..01485cb 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -45,6 +45,7 @@
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
+import com.android.server.display.mode.SyntheticModeManager;
 import com.android.server.display.utils.DebugUtils;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.utils.FoldSettingProvider;
@@ -204,6 +205,7 @@
     private boolean mBootCompleted = false;
     private boolean mInteractive;
     private final DisplayManagerFlags mFlags;
+    private final SyntheticModeManager mSyntheticModeManager;
 
     LogicalDisplayMapper(@NonNull Context context, FoldSettingProvider foldSettingProvider,
             FoldGracePeriodProvider foldGracePeriodProvider,
@@ -213,7 +215,8 @@
         this(context, foldSettingProvider, foldGracePeriodProvider, repo, listener, syncRoot,
                 handler,
                 new DeviceStateToLayoutMap((isDefault) -> isDefault ? DEFAULT_DISPLAY
-                        : sNextNonDefaultDisplayId++, flags), flags);
+                        : sNextNonDefaultDisplayId++, flags), flags,
+                new SyntheticModeManager(flags));
     }
 
     LogicalDisplayMapper(@NonNull Context context, FoldSettingProvider foldSettingProvider,
@@ -221,7 +224,7 @@
             @NonNull DisplayDeviceRepository repo,
             @NonNull Listener listener, @NonNull DisplayManagerService.SyncRoot syncRoot,
             @NonNull Handler handler, @NonNull DeviceStateToLayoutMap deviceStateToLayoutMap,
-            DisplayManagerFlags flags) {
+            DisplayManagerFlags flags, SyntheticModeManager syntheticModeManager) {
         mSyncRoot = syncRoot;
         mPowerManager = context.getSystemService(PowerManager.class);
         mInteractive = mPowerManager.isInteractive();
@@ -241,6 +244,7 @@
         mDisplayDeviceRepo.addListener(this);
         mDeviceStateToLayoutMap = deviceStateToLayoutMap;
         mFlags = flags;
+        mSyntheticModeManager = syntheticModeManager;
     }
 
     @Override
@@ -737,7 +741,7 @@
             mTempDisplayInfo.copyFrom(display.getDisplayInfoLocked());
             display.getNonOverrideDisplayInfoLocked(mTempNonOverrideDisplayInfo);
 
-            display.updateLocked(mDisplayDeviceRepo);
+            display.updateLocked(mDisplayDeviceRepo, mSyntheticModeManager);
             final DisplayInfo newDisplayInfo = display.getDisplayInfoLocked();
             final int updateState = mUpdatedLogicalDisplays.get(displayId, UPDATE_STATE_NEW);
             final boolean wasPreviouslyUpdated = updateState != UPDATE_STATE_NEW;
@@ -1177,7 +1181,7 @@
         final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device,
                 mFlags.isPixelAnisotropyCorrectionInLogicalDisplayEnabled(),
                 mFlags.isAlwaysRotateDisplayDeviceEnabled());
-        display.updateLocked(mDisplayDeviceRepo);
+        display.updateLocked(mDisplayDeviceRepo, mSyntheticModeManager);
 
         final DisplayInfo info = display.getDisplayInfoLocked();
         if (info.type == Display.TYPE_INTERNAL && mDeviceStateToLayoutMap.size() > 1) {
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index cd07f5a..a5414fc 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -164,6 +164,11 @@
             Flags::ignoreAppPreferredRefreshRateRequest
     );
 
+    private final FlagState mSynthetic60hzModes = new FlagState(
+            Flags.FLAG_ENABLE_SYNTHETIC_60HZ_MODES,
+            Flags::enableSynthetic60hzModes
+    );
+
     /**
      * @return {@code true} if 'port' is allowed in display layout configuration file.
      */
@@ -333,6 +338,10 @@
         return mIgnoreAppPreferredRefreshRate.isEnabled();
     }
 
+    public boolean isSynthetic60HzModesEnabled() {
+        return mSynthetic60hzModes.isEnabled();
+    }
+
     /**
      * dumps all flagstates
      * @param pw printWriter
@@ -365,6 +374,8 @@
         pw.println(" " + mResolutionBackupRestore);
         pw.println(" " + mUseFusionProxSensor);
         pw.println(" " + mPeakRefreshRatePhysicalLimit);
+        pw.println(" " + mIgnoreAppPreferredRefreshRate);
+        pw.println(" " + mSynthetic60hzModes);
     }
 
     private static class FlagState {
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index a15a8e8..316b6db 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -266,3 +266,15 @@
       purpose: PURPOSE_BUGFIX
     }
 }
+
+flag {
+    name: "enable_synthetic_60hz_modes"
+    namespace: "display_manager"
+    description: "Feature flag for DisplayManager to enable synthetic 60Hz modes for vrr displays"
+    bug: "338183249"
+    is_fixed_read_only: true
+    metadata {
+      purpose: PURPOSE_BUGFIX
+    }
+}
+
diff --git a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
index 846ee23..e20ac73 100644
--- a/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/mode/DisplayModeDirector.java
@@ -147,6 +147,9 @@
 
     // A map from the display ID to the supported modes on that display.
     private SparseArray<Display.Mode[]> mSupportedModesByDisplay;
+    // A map from the display ID to the app supported modes on that display, might be different from
+    // mSupportedModesByDisplay for VRR displays, used in app mode requests.
+    private SparseArray<Display.Mode[]> mAppSupportedModesByDisplay;
     // A map from the display ID to the default mode of that display.
     private SparseArray<Display.Mode> mDefaultModeByDisplay;
     // a map from display id to display device config
@@ -222,6 +225,7 @@
         mVotesStatsReporter = injector.getVotesStatsReporter(
                 displayManagerFlags.isRefreshRateVotingTelemetryEnabled());
         mSupportedModesByDisplay = new SparseArray<>();
+        mAppSupportedModesByDisplay = new SparseArray<>();
         mDefaultModeByDisplay = new SparseArray<>();
         mAppRequestObserver = new AppRequestObserver(displayManagerFlags);
         mConfigParameterProvider = new DeviceConfigParameterProvider(injector.getDeviceConfig());
@@ -573,6 +577,12 @@
                 final Display.Mode[] modes = mSupportedModesByDisplay.valueAt(i);
                 pw.println("    " + id + " -> " + Arrays.toString(modes));
             }
+            pw.println("  mAppSupportedModesByDisplay:");
+            for (int i = 0; i < mAppSupportedModesByDisplay.size(); i++) {
+                final int id = mAppSupportedModesByDisplay.keyAt(i);
+                final Display.Mode[] modes = mAppSupportedModesByDisplay.valueAt(i);
+                pw.println("    " + id + " -> " + Arrays.toString(modes));
+            }
             pw.println("  mDefaultModeByDisplay:");
             for (int i = 0; i < mDefaultModeByDisplay.size(); i++) {
                 final int id = mDefaultModeByDisplay.keyAt(i);
@@ -638,6 +648,11 @@
     }
 
     @VisibleForTesting
+    void injectAppSupportedModesByDisplay(SparseArray<Display.Mode[]> appSupportedModesByDisplay) {
+        mAppSupportedModesByDisplay = appSupportedModesByDisplay;
+    }
+
+    @VisibleForTesting
     void injectDefaultModeByDisplay(SparseArray<Display.Mode> defaultModeByDisplay) {
         mDefaultModeByDisplay = defaultModeByDisplay;
     }
@@ -1276,7 +1291,7 @@
             Display.Mode[] modes;
             Display.Mode defaultMode;
             synchronized (mLock) {
-                modes = mSupportedModesByDisplay.get(displayId);
+                modes = mAppSupportedModesByDisplay.get(displayId);
                 defaultMode = mDefaultModeByDisplay.get(displayId);
             }
             for (int i = 0; i < modes.length; i++) {
@@ -1289,7 +1304,7 @@
         }
 
         private void setAppRequestedModeLocked(int displayId, int modeId) {
-            final Display.Mode requestedMode = findModeByIdLocked(displayId, modeId);
+            final Display.Mode requestedMode = findAppModeByIdLocked(displayId, modeId);
             if (Objects.equals(requestedMode, mAppRequestedModeByDisplay.get(displayId))) {
                 return;
             }
@@ -1297,10 +1312,17 @@
             final Vote sizeVote;
             if (requestedMode != null) {
                 mAppRequestedModeByDisplay.put(displayId, requestedMode);
-                baseModeRefreshRateVote =
-                        Vote.forBaseModeRefreshRate(requestedMode.getRefreshRate());
                 sizeVote = Vote.forSize(requestedMode.getPhysicalWidth(),
                         requestedMode.getPhysicalHeight());
+                if (requestedMode.isSynthetic()) {
+                    // TODO: for synthetic mode we should not limit frame rate, we must ensure
+                    // that frame rate is reachable within other Votes constraints
+                    baseModeRefreshRateVote = Vote.forRenderFrameRates(
+                            requestedMode.getRefreshRate(), requestedMode.getRefreshRate());
+                } else {
+                    baseModeRefreshRateVote =
+                            Vote.forBaseModeRefreshRate(requestedMode.getRefreshRate());
+                }
             } else {
                 mAppRequestedModeByDisplay.remove(displayId);
                 baseModeRefreshRateVote = null;
@@ -1344,8 +1366,8 @@
                     vote);
         }
 
-        private Display.Mode findModeByIdLocked(int displayId, int modeId) {
-            Display.Mode[] modes = mSupportedModesByDisplay.get(displayId);
+        private Display.Mode findAppModeByIdLocked(int displayId, int modeId) {
+            Display.Mode[] modes = mAppSupportedModesByDisplay.get(displayId);
             if (modes == null) {
                 return null;
             }
@@ -1424,12 +1446,14 @@
 
             // Populate existing displays
             SparseArray<Display.Mode[]> modes = new SparseArray<>();
+            SparseArray<Display.Mode[]> appModes = new SparseArray<>();
             SparseArray<Display.Mode> defaultModes = new SparseArray<>();
             Display[] displays = mInjector.getDisplays();
             for (Display d : displays) {
                 final int displayId = d.getDisplayId();
                 DisplayInfo info = getDisplayInfo(displayId);
                 modes.put(displayId, info.supportedModes);
+                appModes.put(displayId, info.appsSupportedModes);
                 defaultModes.put(displayId, info.getDefaultMode());
             }
             DisplayDeviceConfig defaultDisplayConfig = mDisplayDeviceConfigProvider
@@ -1438,6 +1462,7 @@
                 final int size = modes.size();
                 for (int i = 0; i < size; i++) {
                     mSupportedModesByDisplay.put(modes.keyAt(i), modes.valueAt(i));
+                    mAppSupportedModesByDisplay.put(appModes.keyAt(i), appModes.valueAt(i));
                     mDefaultModeByDisplay.put(defaultModes.keyAt(i), defaultModes.valueAt(i));
                 }
                 mDisplayDeviceConfigByDisplay.put(Display.DEFAULT_DISPLAY, defaultDisplayConfig);
@@ -1459,6 +1484,7 @@
         public void onDisplayRemoved(int displayId) {
             synchronized (mLock) {
                 mSupportedModesByDisplay.remove(displayId);
+                mAppSupportedModesByDisplay.remove(displayId);
                 mDefaultModeByDisplay.remove(displayId);
                 mDisplayDeviceConfigByDisplay.remove(displayId);
                 mSettingsObserver.removeRefreshRateSetting(displayId);
@@ -1619,6 +1645,11 @@
                     mSupportedModesByDisplay.put(displayId, info.supportedModes);
                     changed = true;
                 }
+                if (!Arrays.equals(mAppSupportedModesByDisplay.get(displayId),
+                        info.appsSupportedModes)) {
+                    mAppSupportedModesByDisplay.put(displayId, info.appsSupportedModes);
+                    changed = true;
+                }
                 if (!Objects.equals(mDefaultModeByDisplay.get(displayId), info.getDefaultMode())) {
                     changed = true;
                     mDefaultModeByDisplay.put(displayId, info.getDefaultMode());
diff --git a/services/core/java/com/android/server/display/mode/SyntheticModeManager.java b/services/core/java/com/android/server/display/mode/SyntheticModeManager.java
new file mode 100644
index 0000000..5b6bbc5
--- /dev/null
+++ b/services/core/java/com/android/server/display/mode/SyntheticModeManager.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode;
+
+import android.util.Size;
+import android.view.Display;
+
+import com.android.server.display.DisplayDeviceConfig;
+import com.android.server.display.feature.DisplayManagerFlags;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * When selected by app synthetic modes will only affect render rate switch rather than mode switch
+ */
+public class SyntheticModeManager {
+    private static final float FLOAT_TOLERANCE = 0.01f;
+    private static final float SYNTHETIC_MODE_HIGH_BOUNDARY = 60f + FLOAT_TOLERANCE;
+
+    private final boolean mSynthetic60HzModesEnabled;
+
+    public SyntheticModeManager(DisplayManagerFlags flags) {
+        mSynthetic60HzModesEnabled = flags.isSynthetic60HzModesEnabled();
+    }
+
+    /**
+     * creates display supportedModes array, exposed to applications
+     */
+    public Display.Mode[] createAppSupportedModes(DisplayDeviceConfig config,
+            Display.Mode[] modes) {
+        if (!config.isVrrSupportEnabled() || !mSynthetic60HzModesEnabled) {
+            return modes;
+        }
+        List<Display.Mode> appSupportedModes = new ArrayList<>();
+        Map<Size, int[]> sizes = new LinkedHashMap<>();
+        int nextModeId = 0;
+        // exclude "real" 60Hz modes and below for VRR displays,
+        // they will be replaced with synthetic 60Hz mode
+        // for VRR display there should be "real" mode with rr > 60Hz
+        for (Display.Mode mode : modes) {
+            if (mode.getRefreshRate() > SYNTHETIC_MODE_HIGH_BOUNDARY) {
+                appSupportedModes.add(mode);
+            }
+            if (mode.getModeId() > nextModeId) {
+                nextModeId = mode.getModeId();
+            }
+
+            float divisor = mode.getVsyncRate() / 60f;
+            boolean is60HzAchievable = Math.abs(divisor - Math.round(divisor)) < FLOAT_TOLERANCE;
+            if (is60HzAchievable) {
+                sizes.put(new Size(mode.getPhysicalWidth(), mode.getPhysicalHeight()),
+                        mode.getSupportedHdrTypes());
+            }
+        }
+        // even if VRR display does not have 60Hz mode, we are still adding synthetic 60Hz mode
+        // for each screen size
+        // vsync rate, alternativeRates and hdrTypes  are not important for synthetic mode,
+        // only refreshRate and size are used for DisplayModeDirector votes.
+        for (Map.Entry<Size, int[]> entry: sizes.entrySet()) {
+            nextModeId++;
+            Size size = entry.getKey();
+            int[] hdrTypes = entry.getValue();
+            appSupportedModes.add(
+                    new Display.Mode(nextModeId, size.getWidth(), size.getHeight(), 60f, 60f, true,
+                            new float[0], hdrTypes));
+        }
+        Display.Mode[] appSupportedModesArr = new Display.Mode[appSupportedModes.size()];
+        return appSupportedModes.toArray(appSupportedModesArr);
+    }
+}
diff --git a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
index ad98b4a..a3b1a2d 100644
--- a/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
+++ b/services/core/java/com/android/server/inputmethod/AutofillSuggestionsController.java
@@ -102,26 +102,33 @@
             boolean touchExplorationEnabled) {
         clearPendingInlineSuggestionsRequest();
         mInlineSuggestionsRequestCallback = callback;
-        final InputMethodInfo imi = mService.queryInputMethodForCurrentUserLocked(
-                mService.getSelectedMethodIdLocked());
 
-        if (userId == mService.getCurrentImeUserIdLocked()
-                && imi != null && isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) {
-            mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest(
-                    requestInfo, callback, imi.getPackageName());
-            if (mService.getCurMethodLocked() != null) {
-                // In the normal case when the IME is connected, we can make the request here.
-                performOnCreateInlineSuggestionsRequest();
-            } else {
-                // Otherwise, the next time the IME connection is established,
-                // InputMethodBindingController.mMainConnection#onServiceConnected() will call
-                // into #performOnCreateInlineSuggestionsRequestLocked() to make the request.
-                if (DEBUG) {
-                    Slog.d(TAG, "IME not connected. Delaying inline suggestions request.");
-                }
-            }
-        } else {
+        if (userId != mService.getCurrentImeUserIdLocked()) {
             callback.onInlineSuggestionsUnsupported();
+            return;
+        }
+
+        // Note that current user ID is guaranteed to be userId.
+        final var imeId = mService.getSelectedMethodIdLocked();
+        final InputMethodInfo imi = InputMethodSettingsRepository.get(userId).getMethodMap()
+                .get(imeId);
+        if (imi == null || !isInlineSuggestionsEnabled(imi, touchExplorationEnabled)) {
+            callback.onInlineSuggestionsUnsupported();
+            return;
+        }
+
+        mPendingInlineSuggestionsRequest = new CreateInlineSuggestionsRequest(
+                requestInfo, callback, imi.getPackageName());
+        if (mService.getCurMethodLocked() != null) {
+            // In the normal case when the IME is connected, we can make the request here.
+            performOnCreateInlineSuggestionsRequest();
+        } else {
+            // Otherwise, the next time the IME connection is established,
+            // InputMethodBindingController.mMainConnection#onServiceConnected() will call
+            // into #performOnCreateInlineSuggestionsRequestLocked() to make the request.
+            if (DEBUG) {
+                Slog.d(TAG, "IME not connected. Delaying inline suggestions request.");
+            }
         }
     }
 
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java
index dbdb155..b14702d 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsService.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java
@@ -737,12 +737,13 @@
                     !mUserManager.isQuietModeEnabled(userHandle)) {
                 // Only show notifications for managed profiles once their parent
                 // user is unlocked.
-                showEncryptionNotificationForProfile(userHandle, reason);
+                showEncryptionNotificationForProfile(userHandle, parent.getUserHandle(), reason);
             }
         }
     }
 
-    private void showEncryptionNotificationForProfile(UserHandle user, String reason) {
+    private void showEncryptionNotificationForProfile(UserHandle user, UserHandle parent,
+            String reason) {
         CharSequence title = getEncryptionNotificationTitle();
         CharSequence message = getEncryptionNotificationMessage();
         CharSequence detail = getEncryptionNotificationDetail();
@@ -759,8 +760,15 @@
 
         unlockIntent.setFlags(
                 Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-        PendingIntent intent = PendingIntent.getActivity(mContext, 0, unlockIntent,
-                PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        PendingIntent intent;
+        if (android.app.admin.flags.Flags.hsumUnlockNotificationFix()) {
+            intent = PendingIntent.getActivityAsUser(mContext, 0, unlockIntent,
+                    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED,
+                    null, parent);
+        } else {
+            intent = PendingIntent.getActivity(mContext, 0, unlockIntent,
+                    PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE_UNAUDITED);
+        }
 
         Slogf.d(TAG, "Showing encryption notification for user %d; reason: %s",
                 user.getIdentifier(), reason);
diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
index 71a7d0d..f07b710 100644
--- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
+++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java
@@ -17,6 +17,7 @@
 package com.android.server.os;
 
 import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled;
+import static android.app.admin.flags.Flags.onboardingConsentlessBugreports;
 
 import android.Manifest;
 import android.annotation.NonNull;
@@ -31,6 +32,7 @@
 import android.os.Binder;
 import android.os.BugreportManager.BugreportCallback;
 import android.os.BugreportParams;
+import android.os.Build;
 import android.os.Environment;
 import android.os.IDumpstate;
 import android.os.IDumpstateListener;
@@ -69,12 +71,14 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.PrintWriter;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.OptionalInt;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /**
  * Implementation of the service that provides a privileged API to capture and consume bugreports.
@@ -98,6 +102,9 @@
     private static final String BUGREPORT_SERVICE = "bugreportd";
     private static final long DEFAULT_BUGREPORT_SERVICE_TIMEOUT_MILLIS = 30 * 1000;
 
+    private static final long DEFAULT_BUGREPORT_CONSENTLESS_GRACE_PERIOD_MILLIS =
+            TimeUnit.MINUTES.toMillis(2);
+
     private final Object mLock = new Object();
     private final Injector mInjector;
     private final Context mContext;
@@ -132,6 +139,10 @@
         private ArrayMap<Pair<Integer, String>, ArraySet<String>> mBugreportFiles =
                 new ArrayMap<>();
 
+        // Map of <CallerPackage, Pair<TimestampOfLastConsent, skipConsentForFullReport>>
+        @GuardedBy("mLock")
+        private Map<String, Pair<Long, Boolean>> mConsentGranted = new HashMap<>();
+
         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
         @GuardedBy("mLock")
         final Set<String> mBugreportFilesToPersist = new HashSet<>();
@@ -238,6 +249,64 @@
             }
         }
 
+        /**
+         * Logs an entry with a timestamp of a consent being granted by the user to the calling
+         * {@code packageName}.
+         */
+        void logConsentGrantedForCaller(
+                String packageName, boolean consentGranted, boolean isDeferredReport) {
+            if (!onboardingConsentlessBugreports() || !Build.IS_DEBUGGABLE) {
+                return;
+            }
+            synchronized (mLock) {
+                // Adds an entry with the timestamp of the consent being granted by the user, and
+                // whether the consent can be skipped for a full bugreport, because a single
+                // consent can be used for multiple deferred reports but only one full report.
+                if (consentGranted) {
+                    mConsentGranted.put(packageName, new Pair<>(
+                            System.currentTimeMillis(),
+                            isDeferredReport));
+                } else if (!isDeferredReport) {
+                    if (!mConsentGranted.containsKey(packageName)) {
+                        Slog.e(TAG, "Previous consent from package: " + packageName + " should"
+                                + "have been logged.");
+                        return;
+                    }
+                    mConsentGranted.put(packageName, new Pair<>(
+                            mConsentGranted.get(packageName).first,
+                            /* second = */ false
+                    ));
+                }
+            }
+        }
+
+        /**
+         * Returns {@code true} if user consent be skippeb because a previous consens has been
+         * granted to the caller within the allowed time period.
+         */
+        boolean canSkipConsentScreen(String packageName, boolean isFullReport) {
+            if (!onboardingConsentlessBugreports() || !Build.IS_DEBUGGABLE) {
+                return false;
+            }
+            synchronized (mLock) {
+                if (!mConsentGranted.containsKey(packageName)) {
+                    return false;
+                }
+                long currentTime = System.currentTimeMillis();
+                long consentGrantedTime = mConsentGranted.get(packageName).first;
+                if (consentGrantedTime + DEFAULT_BUGREPORT_CONSENTLESS_GRACE_PERIOD_MILLIS
+                        < currentTime) {
+                    mConsentGranted.remove(packageName);
+                    return false;
+                }
+                boolean skipConsentForFullReport = mConsentGranted.get(packageName).second;
+                if (isFullReport && !skipConsentForFullReport) {
+                    return false;
+                }
+                return true;
+            }
+        }
+
         private void addBugreportMapping(Pair<Integer, String> caller, String bugreportFile) {
             synchronized (mLock) {
                 if (!mBugreportFiles.containsKey(caller)) {
@@ -418,7 +487,7 @@
     public void startBugreport(int callingUidUnused, String callingPackage,
             FileDescriptor bugreportFd, FileDescriptor screenshotFd,
             int bugreportMode, int bugreportFlags, IDumpstateListener listener,
-            boolean isScreenshotRequested) {
+            boolean isScreenshotRequested, boolean skipUserConsentUnused) {
         Objects.requireNonNull(callingPackage);
         Objects.requireNonNull(bugreportFd);
         Objects.requireNonNull(listener);
@@ -509,7 +578,8 @@
     @RequiresPermission(value = Manifest.permission.DUMP, conditional = true)
     public void retrieveBugreport(int callingUidUnused, String callingPackage, int userId,
             FileDescriptor bugreportFd, String bugreportFile,
-            boolean keepBugreportOnRetrievalUnused, IDumpstateListener listener) {
+            boolean keepBugreportOnRetrievalUnused, boolean skipUserConsentUnused,
+            IDumpstateListener listener) {
         int callingUid = Binder.getCallingUid();
         enforcePermission(callingPackage, callingUid, false);
 
@@ -540,9 +610,13 @@
                 return;
             }
 
+            boolean skipUserConsent = mBugreportFileManager.canSkipConsentScreen(
+                    callingPackage, /* isFullReport = */ false);
+
             // Wrap the listener so we can intercept binder events directly.
             DumpstateListener myListener = new DumpstateListener(listener, ds,
-                    new Pair<>(callingUid, callingPackage), /* reportFinishedFile= */ true);
+                    new Pair<>(callingUid, callingPackage), /* reportFinishedFile= */ true,
+                    !skipUserConsent, /* isDeferredReport = */ true);
 
             boolean keepBugreportOnRetrieval = false;
             if (onboardingBugreportV2Enabled()) {
@@ -553,7 +627,7 @@
             setCurrentDumpstateListenerLocked(myListener);
             try {
                 ds.retrieveBugreport(callingUid, callingPackage, userId, bugreportFd,
-                        bugreportFile, keepBugreportOnRetrieval, myListener);
+                        bugreportFile, keepBugreportOnRetrieval, skipUserConsent, myListener);
             } catch (RemoteException e) {
                 Slog.e(TAG, "RemoteException in retrieveBugreport", e);
             }
@@ -754,7 +828,7 @@
             }
         }
 
-        boolean reportFinishedFile =
+        boolean isDeferredConsentReport =
                 (bugreportFlags & BugreportParams.BUGREPORT_FLAG_DEFER_CONSENT) != 0;
 
         boolean keepBugreportOnRetrieval =
@@ -766,14 +840,17 @@
             reportError(listener, IDumpstateListener.BUGREPORT_ERROR_RUNTIME_ERROR);
             return;
         }
-
+        boolean skipUserConsent = mBugreportFileManager.canSkipConsentScreen(
+                callingPackage, !isDeferredConsentReport);
         DumpstateListener myListener = new DumpstateListener(listener, ds,
-                new Pair<>(callingUid, callingPackage), reportFinishedFile,
-                keepBugreportOnRetrieval);
+                new Pair<>(callingUid, callingPackage),
+                /* reportFinishedFile = */ isDeferredConsentReport, keepBugreportOnRetrieval,
+                !isDeferredConsentReport && !skipUserConsent,
+                isDeferredConsentReport);
         setCurrentDumpstateListenerLocked(myListener);
         try {
             ds.startBugreport(callingUid, callingPackage, bugreportFd, screenshotFd, bugreportMode,
-                    bugreportFlags, myListener, isScreenshotRequested);
+                    bugreportFlags, myListener, isScreenshotRequested, skipUserConsent);
         } catch (RemoteException e) {
             // dumpstate service is already started now. We need to kill it to manage the
             // lifecycle correctly. If we don't subsequent callers will get
@@ -930,14 +1007,21 @@
         private boolean mDone;
         private boolean mKeepBugreportOnRetrieval;
 
+        private boolean mConsentGranted;
+
+        private boolean mIsDeferredReport;
+
         DumpstateListener(IDumpstateListener listener, IDumpstate ds,
-                Pair<Integer, String> caller, boolean reportFinishedFile) {
-            this(listener, ds, caller, reportFinishedFile, /* keepBugreportOnRetrieval= */ false);
+                Pair<Integer, String> caller, boolean reportFinishedFile,
+                boolean consentGranted, boolean isDeferredReport) {
+            this(listener, ds, caller, reportFinishedFile, /* keepBugreportOnRetrieval= */ false,
+                    consentGranted, isDeferredReport);
         }
 
         DumpstateListener(IDumpstateListener listener, IDumpstate ds,
                 Pair<Integer, String> caller, boolean reportFinishedFile,
-                boolean keepBugreportOnRetrieval) {
+                boolean keepBugreportOnRetrieval, boolean consentGranted,
+                boolean isDeferredReport) {
             if (DEBUG) {
                 Slogf.d(TAG, "Starting DumpstateListener(id=%d) for caller %s", mId, caller);
             }
@@ -946,6 +1030,8 @@
             mCaller = caller;
             mReportFinishedFile = reportFinishedFile;
             mKeepBugreportOnRetrieval = keepBugreportOnRetrieval;
+            mConsentGranted = consentGranted;
+            mIsDeferredReport = isDeferredReport;
             try {
                 mDs.asBinder().linkToDeath(this, 0);
             } catch (RemoteException e) {
@@ -985,6 +1071,8 @@
             } else if (DEBUG) {
                 Slog.d(TAG, "Not reporting finished file");
             }
+            mBugreportFileManager.logConsentGrantedForCaller(
+                    mCaller.second, mConsentGranted, mIsDeferredReport);
             mListener.onFinished(bugreportFile);
         }
 
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index d053bbb..2f6e07c 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -660,6 +660,8 @@
 
     private final TaskFragment.ConfigOverrideHint mResolveConfigHint;
 
+    private final boolean mOptOutEdgeToEdge;
+
     private static ConstrainDisplayApisConfig sConstrainDisplayApisConfig;
 
     boolean pendingVoiceInteractionStart;   // Waiting for activity-invoked voice session
@@ -2179,9 +2181,12 @@
                     || ent.array.getBoolean(R.styleable.Window_windowShowWallpaper, false);
             mStyleFillsParent = mOccludesParent;
             noDisplay = ent.array.getBoolean(R.styleable.Window_windowNoDisplay, false);
+            mOptOutEdgeToEdge = ent.array.getBoolean(
+                    R.styleable.Window_windowOptOutEdgeToEdgeEnforcement, false);
         } else {
             mStyleFillsParent = mOccludesParent = true;
             noDisplay = false;
+            mOptOutEdgeToEdge = false;
         }
 
         if (options != null) {
@@ -8710,9 +8715,9 @@
         if (rotation == ROTATION_UNDEFINED && !isFixedRotationTransforming()) {
             rotation = mDisplayContent.getRotation();
         }
-        if (!mResolveConfigHint.mUseOverrideInsetsForConfig
-                || getCompatDisplayInsets() != null || shouldCreateCompatDisplayInsets()
-                || isFloating(parentWindowingMode) || rotation == ROTATION_UNDEFINED) {
+        if (!mOptOutEdgeToEdge && (!mResolveConfigHint.mUseOverrideInsetsForConfig
+                || getCompatDisplayInsets() != null || isFloating(parentWindowingMode)
+                || rotation == ROTATION_UNDEFINED)) {
             // If the insets configuration decoupled logic is not enabled for the app, or the app
             // already has a compat override, or the context doesn't contain enough info to
             // calculate the override, skip the override.
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index ca5f26a..125eb2a 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -28,7 +28,6 @@
 import android.graphics.Rect;
 import android.os.Message;
 import android.os.Trace;
-import android.util.Log;
 import android.util.Slog;
 import android.view.DisplayInfo;
 import android.window.DisplayAreaInfo;
@@ -391,6 +390,7 @@
                 || first.defaultModeId != second.defaultModeId
                 || first.userPreferredModeId != second.userPreferredModeId
                 || !Arrays.equals(first.supportedModes, second.supportedModes)
+                || !Arrays.equals(first.appsSupportedModes, second.appsSupportedModes)
                 || first.colorMode != second.colorMode
                 || !Arrays.equals(first.supportedColorModes, second.supportedColorModes)
                 || !Objects.equals(first.hdrCapabilities, second.hdrCapabilities)
diff --git a/services/core/java/com/android/server/wm/RefreshRatePolicy.java b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
index 03574029..8cab7d9 100644
--- a/services/core/java/com/android/server/wm/RefreshRatePolicy.java
+++ b/services/core/java/com/android/server/wm/RefreshRatePolicy.java
@@ -275,7 +275,7 @@
         if (refreshRateSwitchingType != SWITCHING_TYPE_RENDER_FRAME_RATE_ONLY) {
             final int preferredModeId = w.mAttrs.preferredDisplayModeId;
             if (preferredModeId > 0) {
-                for (Display.Mode mode : mDisplayInfo.supportedModes) {
+                for (Display.Mode mode : mDisplayInfo.appsSupportedModes) {
                     if (preferredModeId == mode.getModeId()) {
                         return w.mFrameRateVote.update(mode.getRefreshRate(),
                                 Surface.FRAME_RATE_COMPATIBILITY_EXACT,
diff --git a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
index 42ca7b4..16fcb09 100644
--- a/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
+++ b/services/core/java/com/android/server/wm/SnapshotPersistQueue.java
@@ -348,6 +348,9 @@
                         + bitmap.isMutable() + ") to (config=ARGB_8888, isMutable=false) failed.");
                 return false;
             }
+            final int width = bitmap.getWidth();
+            final int height = bitmap.getHeight();
+            bitmap.recycle();
 
             final File file = mPersistInfoProvider.getHighResolutionBitmapFile(mId, mUserId);
             try {
@@ -365,8 +368,8 @@
             }
 
             final Bitmap lowResBitmap = Bitmap.createScaledBitmap(swBitmap,
-                    (int) (bitmap.getWidth() * mPersistInfoProvider.lowResScaleFactor()),
-                    (int) (bitmap.getHeight() * mPersistInfoProvider.lowResScaleFactor()),
+                    (int) (width * mPersistInfoProvider.lowResScaleFactor()),
+                    (int) (height * mPersistInfoProvider.lowResScaleFactor()),
                     true /* filter */);
             swBitmap.recycle();
 
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index ce53290..2dc439d 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -492,6 +492,27 @@
         return false;
     }
 
+    /** Returns {@code true} if the display contains a transient-launch transition. */
+    boolean hasTransientLaunch(@NonNull DisplayContent dc) {
+        if (mCollectingTransition != null && mCollectingTransition.hasTransientLaunch()
+                && mCollectingTransition.isOnDisplay(dc)) {
+            return true;
+        }
+        for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) {
+            final Transition transition = mWaitingTransitions.get(i);
+            if (transition.hasTransientLaunch() && transition.isOnDisplay(dc)) {
+                return true;
+            }
+        }
+        for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) {
+            final Transition transition = mPlayingTransitions.get(i);
+            if (transition.hasTransientLaunch() && transition.isOnDisplay(dc)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     boolean isTransientHide(@NonNull Task task) {
         if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) {
             return true;
diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java
index 65e1761..3e43f5a 100644
--- a/services/core/java/com/android/server/wm/WallpaperController.java
+++ b/services/core/java/com/android/server/wm/WallpaperController.java
@@ -165,7 +165,7 @@
                             || (w.mActivityRecord != null && !w.mActivityRecord.fillsParent());
                 }
             } else if (w.hasWallpaper() && mService.mPolicy.isKeyguardHostWindow(w.mAttrs)
-                    && w.mTransitionController.isTransitionOnDisplay(mDisplayContent)) {
+                    && w.mTransitionController.hasTransientLaunch(mDisplayContent)) {
                 // If we have no candidates at all, notification shade is allowed to be the target
                 // of last resort even if it has not been made visible yet.
                 if (DEBUG_WALLPAPER) Slog.v(TAG, "Found keyguard as wallpaper target: " + w);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 1a03e78..6d138c5 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -50,6 +50,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
@@ -80,10 +81,10 @@
 
 import com.android.internal.foldables.FoldGracePeriodProvider;
 import com.android.internal.util.test.LocalServiceKeeperRule;
-import com.android.server.LocalServices;
 import com.android.server.display.feature.DisplayManagerFlags;
 import com.android.server.display.layout.DisplayIdProducer;
 import com.android.server.display.layout.Layout;
+import com.android.server.display.mode.SyntheticModeManager;
 import com.android.server.policy.WindowManagerPolicy;
 import com.android.server.utils.FoldSettingProvider;
 
@@ -91,6 +92,7 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.AdditionalAnswers;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
 import org.mockito.Mock;
@@ -141,6 +143,8 @@
     @Mock DisplayManagerFlags mFlagsMock;
     @Mock DisplayAdapter mDisplayAdapterMock;
     @Mock WindowManagerPolicy mWindowManagerPolicy;
+    @Mock
+    SyntheticModeManager mSyntheticModeManagerMock;
 
     @Captor ArgumentCaptor<LogicalDisplay> mDisplayCaptor;
     @Captor ArgumentCaptor<Integer> mDisplayEventCaptor;
@@ -196,6 +200,8 @@
         when(mResourcesMock.getIntArray(
                 com.android.internal.R.array.config_deviceStatesOnWhichToSleep))
                 .thenReturn(new int[]{0});
+        when(mSyntheticModeManagerMock.createAppSupportedModes(any(), any())).thenAnswer(
+                AdditionalAnswers.returnsSecondArg());
 
         when(mFlagsMock.isConnectedDisplayManagementEnabled()).thenReturn(false);
         mLooper = new TestLooper();
@@ -204,7 +210,7 @@
                 mFoldGracePeriodProvider,
                 mDisplayDeviceRepo,
                 mListenerMock, new DisplayManagerService.SyncRoot(), mHandler,
-                mDeviceStateToLayoutMapSpy, mFlagsMock);
+                mDeviceStateToLayoutMapSpy, mFlagsMock, mSyntheticModeManagerMock);
         mLogicalDisplayMapper.onWindowManagerReady();
     }
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
index 779445e..8936f06 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
@@ -40,9 +41,11 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.server.display.layout.Layout;
+import com.android.server.display.mode.SyntheticModeManager;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.AdditionalAnswers;
 
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -54,6 +57,7 @@
     private static final int DISPLAY_WIDTH = 100;
     private static final int DISPLAY_HEIGHT = 200;
     private static final int MODE_ID = 1;
+    private static final int OTHER_MODE_ID = 2;
 
     private LogicalDisplay mLogicalDisplay;
     private DisplayDevice mDisplayDevice;
@@ -61,6 +65,7 @@
     private Context mContext;
     private IBinder mDisplayToken;
     private DisplayDeviceRepository mDeviceRepo;
+    private SyntheticModeManager mSyntheticModeManager;
     private final DisplayDeviceInfo mDisplayDeviceInfo = new DisplayDeviceInfo();
 
     @Before
@@ -71,6 +76,7 @@
         mDisplayAdapter = mock(DisplayAdapter.class);
         mContext = mock(Context.class);
         mDisplayToken = mock(IBinder.class);
+        mSyntheticModeManager = mock(SyntheticModeManager.class);
         mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice);
 
         mDisplayDeviceInfo.copyFrom(new DisplayDeviceInfo());
@@ -81,6 +87,8 @@
         mDisplayDeviceInfo.supportedModes = new Display.Mode[] {new Display.Mode(MODE_ID,
                 DISPLAY_WIDTH, DISPLAY_HEIGHT, /* refreshRate= */ 60)};
         when(mDisplayDevice.getDisplayDeviceInfoLocked()).thenReturn(mDisplayDeviceInfo);
+        when(mSyntheticModeManager.createAppSupportedModes(any(), any())).thenAnswer(
+                AdditionalAnswers.returnsSecondArg());
 
         // Disable binder caches in this process.
         PropertyInvalidatedCache.disableForTestMode();
@@ -102,7 +110,7 @@
                     public void finishWrite(OutputStream os, boolean success) {}
                 }));
         mDeviceRepo.onDisplayDeviceEvent(mDisplayDevice, DisplayAdapter.DISPLAY_DEVICE_EVENT_ADDED);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
     }
 
     @Test
@@ -111,7 +119,7 @@
         mDisplayDeviceInfo.xDpi = 0.5f;
         mDisplayDeviceInfo.yDpi = 1.0f;
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth);
         assertEquals(DISPLAY_HEIGHT, originalDisplayInfo.logicalHeight);
@@ -156,7 +164,7 @@
         mDisplayDeviceInfo.xDpi = 0.5f;
         mDisplayDeviceInfo.yDpi = 1.0f;
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         // Content width not scaled
         assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth);
@@ -185,7 +193,7 @@
         mDisplayDeviceInfo.xDpi = 0.5f;
         mDisplayDeviceInfo.yDpi = 1.0f;
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         // Content width re-scaled
         assertEquals(DISPLAY_WIDTH * 2, originalDisplayInfo.logicalWidth);
@@ -214,7 +222,7 @@
         mDisplayDeviceInfo.xDpi = 1.0f;
         mDisplayDeviceInfo.yDpi = 0.5f;
         mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
 
         SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
         mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
@@ -234,7 +242,7 @@
         displayInfo.logicalHeight = DISPLAY_HEIGHT;
         mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
         mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
 
         var updatedDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         assertEquals(Surface.ROTATION_90, updatedDisplayInfo.rotation);
@@ -277,7 +285,7 @@
         mDisplayDeviceInfo.xDpi = 0.5f;
         mDisplayDeviceInfo.yDpi = 1.0f;
         mLogicalDisplay.setDisplayInfoOverrideFromWindowManagerLocked(displayInfo);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
 
         SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
         mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false);
@@ -301,7 +309,7 @@
         mDisplayDeviceInfo.xDpi = 1.0f;
         mDisplayDeviceInfo.yDpi = 0.5f;
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         var originalDisplayInfo = mLogicalDisplay.getDisplayInfoLocked();
         // Content width re-scaled
         assertEquals(DISPLAY_WIDTH, originalDisplayInfo.logicalWidth);
@@ -341,7 +349,7 @@
 
         expectedPosition.set(40, -20);
         mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         displayInfo.logicalWidth = DISPLAY_HEIGHT;
         displayInfo.logicalHeight = DISPLAY_WIDTH;
         displayInfo.rotation = Surface.ROTATION_90;
@@ -356,7 +364,7 @@
         mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice,
                 /*isAnisotropyCorrectionEnabled=*/ true,
                 /*isAlwaysRotateDisplayDeviceEnabled=*/ true);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         Point expectedPosition = new Point();
 
         SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class);
@@ -383,7 +391,7 @@
 
         expectedPosition.set(40, -20);
         mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT;
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         displayInfo.logicalWidth = DISPLAY_HEIGHT;
         displayInfo.logicalHeight = DISPLAY_WIDTH;
         displayInfo.rotation = Surface.ROTATION_90;
@@ -444,7 +452,7 @@
         // Update position and test to see that it's been updated to a rear, presentation display
         // that destroys content on removal
         mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_REAR);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertEquals(Display.FLAG_REAR | Display.FLAG_PRESENTATION,
                 mLogicalDisplay.getDisplayInfoLocked().flags);
         assertEquals(Display.REMOVE_MODE_DESTROY_CONTENT,
@@ -452,7 +460,7 @@
 
         // And then check the unsetting the position resets both
         mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_UNKNOWN);
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertEquals(0, mLogicalDisplay.getDisplayInfoLocked().flags);
         assertEquals(Display.REMOVE_MODE_MOVE_CONTENT_TO_PRIMARY,
                 mLogicalDisplay.getDisplayInfoLocked().removeMode);
@@ -468,7 +476,7 @@
         // Display info should only be updated when updateLocked is called
         assertEquals(info2, info1);
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
         assertNotEquals(info3, info2);
         assertEquals(layoutLimitedRefreshRate, info3.layoutLimitedRefreshRate);
@@ -483,7 +491,7 @@
         mLogicalDisplay.updateLayoutLimitedRefreshRateLocked(layoutLimitedRefreshRate);
         assertTrue(mLogicalDisplay.isDirtyLocked());
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertFalse(mLogicalDisplay.isDirtyLocked());
     }
 
@@ -497,7 +505,7 @@
         // Display info should only be updated when updateLocked is called
         assertEquals(info2, info1);
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
         assertNotEquals(info3, info2);
         assertTrue(refreshRanges.contentEquals(info3.thermalRefreshRateThrottling));
@@ -512,7 +520,7 @@
         mLogicalDisplay.updateThermalRefreshRateThrottling(refreshRanges);
         assertTrue(mLogicalDisplay.isDirtyLocked());
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertFalse(mLogicalDisplay.isDirtyLocked());
     }
 
@@ -525,7 +533,7 @@
         // Display info should only be updated when updateLocked is called
         assertEquals(info2, info1);
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
         assertNotEquals(info3, info2);
         assertEquals(newId, info3.displayGroupId);
@@ -538,7 +546,7 @@
         mLogicalDisplay.updateDisplayGroupIdLocked(99);
         assertTrue(mLogicalDisplay.isDirtyLocked());
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertFalse(mLogicalDisplay.isDirtyLocked());
     }
 
@@ -551,7 +559,7 @@
         // Display info should only be updated when updateLocked is called
         assertEquals(info2, info1);
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         DisplayInfo info3 = mLogicalDisplay.getDisplayInfoLocked();
         assertNotEquals(info3, info2);
         assertEquals(brightnessThrottlingDataId, info3.thermalBrightnessThrottlingDataId);
@@ -564,7 +572,20 @@
         mLogicalDisplay.setThermalBrightnessThrottlingDataIdLocked("99");
         assertTrue(mLogicalDisplay.isDirtyLocked());
 
-        mLogicalDisplay.updateLocked(mDeviceRepo);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
         assertFalse(mLogicalDisplay.isDirtyLocked());
     }
+
+    @Test
+    public void testGetsAppSupportedModesFromSyntheticModeManager() {
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice);
+        Display.Mode[] appSupportedModes = new Display.Mode[] {new Display.Mode(OTHER_MODE_ID,
+                DISPLAY_WIDTH, DISPLAY_HEIGHT, /* refreshRate= */ 45)};
+        when(mSyntheticModeManager.createAppSupportedModes(
+                any(), eq(mDisplayDeviceInfo.supportedModes))).thenReturn(appSupportedModes);
+
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
+        DisplayInfo info = mLogicalDisplay.getDisplayInfoLocked();
+        assertArrayEquals(appSupportedModes, info.appsSupportedModes);
+    }
 }
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
index f0abcd2..cf6146f 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/AppRequestObserverTest.kt
@@ -58,11 +58,14 @@
         val modes = arrayOf(
             Display.Mode(1, 1000, 1000, 60f),
             Display.Mode(2, 1000, 1000, 90f),
-            Display.Mode(3, 1000, 1000, 120f)
+            Display.Mode(3, 1000, 1000, 120f),
+            Display.Mode(99, 1000, 1000, 45f, 45f, true, floatArrayOf(), intArrayOf())
         )
-        displayModeDirector.injectSupportedModesByDisplay(SparseArray<Array<Display.Mode>>().apply {
-            append(Display.DEFAULT_DISPLAY, modes)
-        })
+
+        displayModeDirector.injectAppSupportedModesByDisplay(
+            SparseArray<Array<Display.Mode>>().apply {
+                append(Display.DEFAULT_DISPLAY, modes)
+            })
         displayModeDirector.injectDefaultModeByDisplay(SparseArray<Display.Mode>().apply {
             append(Display.DEFAULT_DISPLAY, modes[0])
         })
@@ -116,7 +119,9 @@
             BaseModeRefreshRateVote(60f), SizeVote(1000, 1000, 1000, 1000), null),
         PREFERRED_REFRESH_RATE_IGNORED(true, 0, 60f, 0f, 0f,
             null, null, null),
-        PREFERRED_REFRESH_RATE_INVALID(false, 0, 45f, 0f, 0f,
+        PREFERRED_REFRESH_RATE_INVALID(false, 0, 25f, 0f, 0f,
             null, null, null),
+        SYNTHETIC_MODE(false, 99, 0f, 0f, 0f,
+            RenderVote(45f, 45f), SizeVote(1000, 1000, 1000, 1000), null),
     }
 }
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt
new file mode 100644
index 0000000..5cd3a33
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/SyntheticModeManagerTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.mode
+
+import android.view.Display.Mode
+import com.android.server.display.DisplayDeviceConfig
+import com.android.server.display.feature.DisplayManagerFlags
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import com.google.testing.junit.testparameterinjector.TestParameter
+import com.google.testing.junit.testparameterinjector.TestParameterInjector
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+private val DISPLAY_MODES = arrayOf(
+    Mode(1, 100, 100, 60f),
+    Mode(2, 100, 100, 120f)
+)
+
+@SmallTest
+@RunWith(TestParameterInjector::class)
+class SyntheticModeManagerTest {
+
+    private val mockFlags = mock<DisplayManagerFlags>()
+    private val mockConfig = mock<DisplayDeviceConfig>()
+
+    @Test
+    fun `test app supported modes`(@TestParameter testCase: AppSupportedModesTestCase) {
+        whenever(mockFlags.isSynthetic60HzModesEnabled).thenReturn(testCase.syntheticModesEnabled)
+        whenever(mockConfig.isVrrSupportEnabled).thenReturn(testCase.vrrSupported)
+        val syntheticModeManager = SyntheticModeManager(mockFlags)
+
+        val result = syntheticModeManager.createAppSupportedModes(
+            mockConfig, testCase.supportedModes)
+
+        assertThat(result).isEqualTo(testCase.expectedAppModes)
+    }
+
+    enum class AppSupportedModesTestCase(
+        val syntheticModesEnabled: Boolean,
+        val vrrSupported: Boolean,
+        val supportedModes: Array<Mode>,
+        val expectedAppModes: Array<Mode>
+    ) {
+        SYNTHETIC_MODES_NOT_SUPPORTED(false, true, DISPLAY_MODES, DISPLAY_MODES),
+        VRR_NOT_SUPPORTED(true, false, DISPLAY_MODES, DISPLAY_MODES),
+        VRR_SYNTHETIC_NOT_SUPPORTED(false, false, DISPLAY_MODES, DISPLAY_MODES),
+        SINGLE_RESOLUTION_MODES(true, true, DISPLAY_MODES, arrayOf(
+            Mode(2, 100, 100, 120f),
+            Mode(3, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf())
+        )),
+        NO_60HZ_MODES(true, true, arrayOf(Mode(2, 100, 100, 120f)),
+            arrayOf(
+                Mode(2, 100, 100, 120f),
+                Mode(3, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf())
+            )
+        ),
+        MULTI_RESOLUTION_MODES(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 120f),
+                Mode(2, 200, 200, 60f),
+                Mode(3, 300, 300, 60f),
+                Mode(4, 300, 300, 90f),
+                ),
+            arrayOf(
+                Mode(1, 100, 100, 120f),
+                Mode(4, 300, 300, 90f),
+                Mode(5, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf()),
+                Mode(6, 200, 200, 60f, 60f, true, floatArrayOf(), intArrayOf()),
+                Mode(7, 300, 300, 60f, 60f, true, floatArrayOf(), intArrayOf())
+            )
+        ),
+        WITH_HDR_TYPES(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 120f, 120f, false, floatArrayOf(), intArrayOf(1, 2)),
+                Mode(2, 200, 200, 60f, 120f, false, floatArrayOf(), intArrayOf(3, 4)),
+                Mode(3, 200, 200, 120f, 120f, false, floatArrayOf(), intArrayOf(5, 6)),
+            ),
+            arrayOf(
+                Mode(1, 100, 100, 120f, 120f, false, floatArrayOf(), intArrayOf(1, 2)),
+                Mode(3, 200, 200, 120f, 120f, false, floatArrayOf(), intArrayOf(5, 6)),
+                Mode(4, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf(1, 2)),
+                Mode(5, 200, 200, 60f, 60f, true, floatArrayOf(), intArrayOf(5, 6)),
+            )
+        ),
+        UNACHIEVABLE_60HZ(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 90f),
+            ),
+            arrayOf(
+                Mode(1, 100, 100, 90f),
+            )
+        ),
+        MULTI_RESOLUTION_MODES_WITH_UNACHIEVABLE_60HZ(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 120f),
+                Mode(2, 200, 200, 90f),
+            ),
+            arrayOf(
+                Mode(1, 100, 100, 120f),
+                Mode(2, 200, 200, 90f),
+                Mode(3, 100, 100, 60f, 60f, true, floatArrayOf(), intArrayOf()),
+            )
+        ),
+        LOWER_THAN_60HZ_MODES(true, true,
+            arrayOf(
+                Mode(1, 100, 100, 30f),
+                Mode(2, 100, 100, 45f),
+                Mode(3, 100, 100, 90f),
+            ),
+            arrayOf(
+                Mode(3, 100, 100, 90f),
+            )
+        ),
+    }
+}
\ No newline at end of file
diff --git a/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt b/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt
index 04b35f1..5da1bb6 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/mode/VoteSummaryTest.kt
@@ -154,7 +154,7 @@
     }
 }
 private fun createMode(modeId: Int, refreshRate: Float, vsyncRate: Float): Display.Mode {
-    return Display.Mode(modeId, 600, 800, refreshRate, vsyncRate,
+    return Display.Mode(modeId, 600, 800, refreshRate, vsyncRate, false,
             FloatArray(0), IntArray(0))
 }
 
diff --git a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
index 9862663..1db97b9 100644
--- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java
@@ -186,7 +186,8 @@
                 new FileDescriptor(), /* screenshotFd= */ null,
                 BugreportParams.BUGREPORT_MODE_FULL,
                 /* flags= */ 0, new Listener(new CountDownLatch(1)),
-                /* isScreenshotRequested= */ false);
+                /* isScreenshotRequested= */ false,
+                /* skipUserConsentUnused = */ false);
 
         assertThat(mInjector.isBugreportStarted()).isTrue();
     }
@@ -202,7 +203,8 @@
                 new FileDescriptor(), /* screenshotFd= */ null,
                 BugreportParams.BUGREPORT_MODE_FULL,
                 /* flags= */ 0, new Listener(new CountDownLatch(1)),
-                /* isScreenshotRequested= */ false);
+                /* isScreenshotRequested= */ false,
+                /* skipUserConsentUnused = */ false);
 
         assertThat(mInjector.isBugreportStarted()).isTrue();
     }
@@ -216,7 +218,8 @@
                         new FileDescriptor(), /* screenshotFd= */ null,
                         BugreportParams.BUGREPORT_MODE_FULL,
                         /* flags= */ 0, new Listener(new CountDownLatch(1)),
-                        /* isScreenshotRequested= */ false));
+                        /* isScreenshotRequested= */ false,
+                        /* skipUserConsentUnused = */ false));
 
         assertThat(thrown.getMessage()).contains("not an admin user");
     }
@@ -232,7 +235,8 @@
                         new FileDescriptor(), /* screenshotFd= */ null,
                         BugreportParams.BUGREPORT_MODE_REMOTE,
                         /* flags= */ 0, new Listener(new CountDownLatch(1)),
-                        /* isScreenshotRequested= */ false));
+                        /* isScreenshotRequested= */ false,
+                        /* skipUserConsentUnused = */ false));
 
         assertThat(thrown.getMessage()).contains("not affiliated to the device owner");
     }
@@ -243,7 +247,7 @@
         Listener listener = new Listener(latch);
         mService.retrieveBugreport(Binder.getCallingUid(), mContext.getPackageName(),
                 mContext.getUserId(), new FileDescriptor(), mBugreportFile,
-                /* keepOnRetrieval= */ false, listener);
+                /* keepOnRetrieval= */ false, /* skipUserConsent = */ false, listener);
         assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue();
         assertThat(listener.getErrorCode()).isEqualTo(
                 BugreportCallback.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE);
diff --git a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
index c9a83b0..7ebf9ac 100644
--- a/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/RefreshRatePolicyTest.java
@@ -109,6 +109,7 @@
                         defaultMode.getPhysicalWidth(), defaultMode.getPhysicalHeight(),
                         LOW_REFRESH_RATE),
         };
+        mDisplayInfo.appsSupportedModes = mDisplayInfo.supportedModes;
         mDisplayInfo.defaultModeId = HI_MODE_ID;
         mPolicy = new RefreshRatePolicy(mWm, mDisplayInfo, mDenylist);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
index 5b1a18d..9b48cb9 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WallpaperControllerTests.java
@@ -314,6 +314,18 @@
         // Wallpaper is invisible because the lowest show-when-locked activity is opaque.
         assertNull(wallpaperController.getWallpaperTarget());
 
+        // Only transient-launch transition will make notification shade as last resort target.
+        // This verifies that regular transition won't choose invisible keyguard as the target.
+        final WindowState keyguard = createWindow(null /* parent */,
+                WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE, "keyguard");
+        keyguard.mAttrs.flags |= FLAG_SHOW_WALLPAPER;
+        registerTestTransitionPlayer();
+        final Transition transition = wallpaperWindow.mTransitionController.createTransition(
+                WindowManager.TRANSIT_CHANGE);
+        transition.collect(keyguard);
+        wallpaperController.adjustWallpaperWindows();
+        assertNull(wallpaperController.getWallpaperTarget());
+
         // A show-when-locked wallpaper is used for lockscreen. So the top wallpaper should
         // be the one that is not show-when-locked.
         final WindowState wallpaperWindow2 = createWallpaperWindow(mDisplayContent);