Merge "Create RotaryInputValueView to display the most recent rotary input value." into main
diff --git a/core/java/android/app/Dialog.java b/core/java/android/app/Dialog.java
index 4851279..d0d76a4 100644
--- a/core/java/android/app/Dialog.java
+++ b/core/java/android/app/Dialog.java
@@ -454,12 +454,11 @@
      */
     protected void onStart() {
         if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(true);
-        if (mContext != null
+        if (allowsRegisterDefaultOnBackInvokedCallback() && mContext != null
                 && WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
             // Add onBackPressed as default back behavior.
             mDefaultBackCallback = this::onBackPressed;
             getOnBackInvokedDispatcher().registerSystemOnBackInvokedCallback(mDefaultBackCallback);
-            mDefaultBackCallback = null;
         }
     }
 
@@ -470,9 +469,18 @@
         if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(false);
         if (mDefaultBackCallback != null) {
             getOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mDefaultBackCallback);
+            mDefaultBackCallback = null;
         }
     }
 
+    /**
+     * Whether this dialog allows to register the default onBackInvokedCallback.
+     * @hide
+     */
+    protected boolean allowsRegisterDefaultOnBackInvokedCallback() {
+        return true;
+    }
+
     private static final String DIALOG_SHOWING_TAG = "android:dialogShowing";
     private static final String DIALOG_HIERARCHY_TAG = "android:dialogHierarchy";
 
@@ -697,7 +705,8 @@
         if (event.isTracking() && !event.isCanceled()) {
             switch (keyCode) {
                 case KeyEvent.KEYCODE_BACK:
-                    if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)) {
+                    if (!WindowOnBackInvokedDispatcher.isOnBackInvokedCallbackEnabled(mContext)
+                            || !allowsRegisterDefaultOnBackInvokedCallback()) {
                         onBackPressed();
                         return true;
                     }
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 892b45e..7d2ef4d 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -12819,7 +12819,6 @@
                 } else {
                     mBackgroundColor = rawColor;
                 }
-                mProtectionColor = COLOR_INVALID;  // filled in at the end
                 mPrimaryTextColor = ContrastColorUtil.findAlphaToMeetContrast(
                         ContrastColorUtil.resolvePrimaryColor(ctx, mBackgroundColor, nightMode),
                         mBackgroundColor, 4.5);
@@ -12836,7 +12835,6 @@
             } else {
                 int[] attrs = {
                         R.attr.colorSurface,
-                        R.attr.colorBackgroundFloating,
                         R.attr.textColorPrimary,
                         R.attr.textColorSecondary,
                         R.attr.colorAccent,
@@ -12848,15 +12846,14 @@
                 };
                 try (TypedArray ta = obtainDayNightAttributes(ctx, attrs)) {
                     mBackgroundColor = getColor(ta, 0, nightMode ? Color.BLACK : Color.WHITE);
-                    mProtectionColor = getColor(ta, 1, COLOR_INVALID);
-                    mPrimaryTextColor = getColor(ta, 2, COLOR_INVALID);
-                    mSecondaryTextColor = getColor(ta, 3, COLOR_INVALID);
-                    mPrimaryAccentColor = getColor(ta, 4, COLOR_INVALID);
-                    mSecondaryAccentColor = getColor(ta, 5, COLOR_INVALID);
-                    mTertiaryAccentColor = getColor(ta, 6, COLOR_INVALID);
-                    mOnAccentTextColor = getColor(ta, 7, COLOR_INVALID);
-                    mErrorColor = getColor(ta, 8, COLOR_INVALID);
-                    mRippleAlpha = Color.alpha(getColor(ta, 9, 0x33ffffff));
+                    mPrimaryTextColor = getColor(ta, 1, COLOR_INVALID);
+                    mSecondaryTextColor = getColor(ta, 2, COLOR_INVALID);
+                    mPrimaryAccentColor = getColor(ta, 3, COLOR_INVALID);
+                    mSecondaryAccentColor = getColor(ta, 4, COLOR_INVALID);
+                    mTertiaryAccentColor = getColor(ta, 5, COLOR_INVALID);
+                    mOnAccentTextColor = getColor(ta, 6, COLOR_INVALID);
+                    mErrorColor = getColor(ta, 7, COLOR_INVALID);
+                    mRippleAlpha = Color.alpha(getColor(ta, 8, 0x33ffffff));
                 }
                 mContrastColor = calculateContrastColor(ctx, rawColor, mPrimaryAccentColor,
                         mBackgroundColor, nightMode);
@@ -12889,9 +12886,7 @@
                 }
             }
             // make sure every color has a valid value
-            if (mProtectionColor == COLOR_INVALID) {
-                mProtectionColor = ColorUtils.blendARGB(mPrimaryTextColor, mBackgroundColor, 0.8f);
-            }
+            mProtectionColor = ColorUtils.blendARGB(mPrimaryTextColor, mBackgroundColor, 0.9f);
         }
 
         /** calculates the contrast color for the non-colorized notifications */
diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java
index c3df17d..529363f 100644
--- a/core/java/android/content/pm/CrossProfileApps.java
+++ b/core/java/android/content/pm/CrossProfileApps.java
@@ -344,15 +344,22 @@
         // If there is a label for the launcher intent, then use that as it is typically shorter.
         // Otherwise, just use the top-level application name.
         Intent launchIntent = pm.getLaunchIntentForPackage(mContext.getPackageName());
+        if (launchIntent == null) {
+            return getDefaultCallingApplicationLabel();
+        }
         List<ResolveInfo> infos =
                 pm.queryIntentActivities(
                         launchIntent, PackageManager.ResolveInfoFlags.of(MATCH_DEFAULT_ONLY));
         if (infos.size() > 0) {
             return infos.get(0).loadLabel(pm);
         }
+        return getDefaultCallingApplicationLabel();
+    }
+
+    private CharSequence getDefaultCallingApplicationLabel() {
         return mContext.getApplicationInfo()
                 .loadSafeLabel(
-                        pm,
+                        mContext.getPackageManager(),
                         /* ellipsizeDip= */ 0,
                         TextUtils.SAFE_STRING_FLAG_SINGLE_LINE
                                 | TextUtils.SAFE_STRING_FLAG_TRIM);
diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java
index 795eb4a..8f653b3 100644
--- a/core/java/android/inputmethodservice/InputMethodService.java
+++ b/core/java/android/inputmethodservice/InputMethodService.java
@@ -576,6 +576,12 @@
     @EnabledSince(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
     public static final long DISALLOW_INPUT_METHOD_INTERFACE_OVERRIDE = 148086656L;
 
+    /**
+     * Enable the logic to allow hiding the IME caption bar ("fake" IME navigation bar).
+     * @hide
+     */
+    public static final boolean ENABLE_HIDE_IME_CAPTION_BAR = true;
+
     LayoutInflater mInflater;
     TypedArray mThemeAttrs;
     @UnsupportedAppUsage
diff --git a/core/java/android/inputmethodservice/NavigationBarController.java b/core/java/android/inputmethodservice/NavigationBarController.java
index 78388ef..c01664e 100644
--- a/core/java/android/inputmethodservice/NavigationBarController.java
+++ b/core/java/android/inputmethodservice/NavigationBarController.java
@@ -16,6 +16,8 @@
 
 package android.inputmethodservice;
 
+import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR;
+import static android.view.WindowInsets.Type.captionBar;
 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS;
 
 import android.animation.ValueAnimator;
@@ -230,6 +232,16 @@
 
             setIconTintInternal(calculateTargetDarkIntensity(mAppearance,
                     mDrawLegacyNavigationBarBackground));
+
+            if (ENABLE_HIDE_IME_CAPTION_BAR) {
+                mNavigationBarFrame.setOnApplyWindowInsetsListener((view, insets) -> {
+                    if (mNavigationBarFrame != null) {
+                        boolean visible = insets.isVisible(captionBar());
+                        mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
+                    }
+                    return view.onApplyWindowInsets(insets);
+                });
+            }
         }
 
         private void uninstallNavigationBarFrameIfNecessary() {
@@ -240,6 +252,9 @@
             if (parent instanceof ViewGroup) {
                 ((ViewGroup) parent).removeView(mNavigationBarFrame);
             }
+            if (ENABLE_HIDE_IME_CAPTION_BAR) {
+                mNavigationBarFrame.setOnApplyWindowInsetsListener(null);
+            }
             mNavigationBarFrame = null;
         }
 
@@ -414,7 +429,9 @@
                         decor.bringChildToFront(mNavigationBarFrame);
                     }
                 }
-                mNavigationBarFrame.setVisibility(View.VISIBLE);
+                if (!ENABLE_HIDE_IME_CAPTION_BAR) {
+                    mNavigationBarFrame.setVisibility(View.VISIBLE);
+                }
             }
         }
 
@@ -435,6 +452,11 @@
                     mShouldShowImeSwitcherWhenImeIsShown;
             mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown;
 
+            if (ENABLE_HIDE_IME_CAPTION_BAR) {
+                mService.mWindow.getWindow().getDecorView().getWindowInsetsController()
+                        .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight());
+            }
+
             if (imeDrawsImeNavBar) {
                 installNavigationBarFrameIfNecessary();
                 if (mNavigationBarFrame == null) {
@@ -528,6 +550,16 @@
             return drawLegacyNavigationBarBackground;
         }
 
+        /**
+         * Returns the height of the IME caption bar if this should be shown, or {@code 0} instead.
+         */
+        private int getImeCaptionBarHeight() {
+            return mImeDrawsImeNavBar
+                    ? mService.getResources().getDimensionPixelSize(
+                            com.android.internal.R.dimen.navigation_bar_frame_height)
+                    : 0;
+        }
+
         @Override
         public String toDebugString() {
             return "{mImeDrawsImeNavBar=" + mImeDrawsImeNavBar
diff --git a/core/java/android/inputmethodservice/SoftInputWindow.java b/core/java/android/inputmethodservice/SoftInputWindow.java
index 5704dac..e4a09a6 100644
--- a/core/java/android/inputmethodservice/SoftInputWindow.java
+++ b/core/java/android/inputmethodservice/SoftInputWindow.java
@@ -79,6 +79,13 @@
     @WindowState
     private int mWindowState = WindowState.TOKEN_PENDING;
 
+    @Override
+    protected boolean allowsRegisterDefaultOnBackInvokedCallback() {
+        // Do not register OnBackInvokedCallback from Dialog#onStart, InputMethodService will
+        // register CompatOnBackInvokedCallback for input method window.
+        return false;
+    }
+
     /**
      * Set {@link IBinder} window token to the window.
      *
diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java
index 3bdaca9..e287bd9 100644
--- a/core/java/android/text/DynamicLayout.java
+++ b/core/java/android/text/DynamicLayout.java
@@ -622,7 +622,7 @@
             sBuilder = null;
         }
 
-        if (reflowed == null) {
+        if (b == null) {
             b = StaticLayout.Builder.obtain(text, where, where + after, getPaint(), getWidth());
         }
 
@@ -641,7 +641,7 @@
                 .setAddLastLineLineSpacing(!islast)
                 .setIncludePad(false);
 
-        reflowed = b.regenerate(true /* trackpadding */, reflowed);
+        reflowed = b.buildPartialStaticLayoutForDynamicLayout(true /* trackpadding */, reflowed);
         int n = reflowed.getLineCount();
         // If the new layout has a blank line at the end, but it is not
         // the very end of the buffer, then we already have a line that
diff --git a/core/java/android/text/StaticLayout.java b/core/java/android/text/StaticLayout.java
index f843900..3d1895c 100644
--- a/core/java/android/text/StaticLayout.java
+++ b/core/java/android/text/StaticLayout.java
@@ -437,13 +437,25 @@
             return result;
         }
 
-        /* package */ @NonNull StaticLayout regenerate(boolean trackpadding, StaticLayout recycle) {
+        /**
+         * DO NOT USE THIS METHOD OTHER THAN DynamicLayout.
+         *
+         * This class generates a very weird StaticLayout only for getting a result of line break.
+         * Since DynamicLayout keeps StaticLayout reference in the static context for object
+         * recycling but keeping text reference in static context will end up with leaking Context
+         * due to TextWatcher via TextView.
+         *
+         * So, this is a dirty work around that creating StaticLayout without passing text reference
+         * to the super constructor, but calculating the text layout by calling generate function
+         * directly.
+         */
+        /* package */ @NonNull StaticLayout buildPartialStaticLayoutForDynamicLayout(
+                boolean trackpadding, StaticLayout recycle) {
             if (recycle == null) {
-                return new StaticLayout(this, trackpadding, COLUMNS_ELLIPSIZE);
-            } else {
-                recycle.generate(this, mIncludePad, trackpadding);
-                return recycle;
+                recycle = new StaticLayout();
             }
+            recycle.generate(this, mIncludePad, trackpadding);
+            return recycle;
         }
 
         private CharSequence mText;
@@ -474,6 +486,37 @@
     }
 
     /**
+     * DO NOT USE THIS CONSTRUCTOR OTHER THAN FOR DYNAMIC LAYOUT.
+     * See Builder#buildPartialStaticLayoutForDynamicLayout for the reason of this constructor.
+     */
+    private StaticLayout() {
+        super(
+                null,  // text
+                null,  // paint
+                0,  // width
+                null, // alignment
+                null, // textDir
+                1, // spacing multiplier
+                0, // spacing amount
+                false, // include font padding
+                false, // fallback line spacing
+                0,  // ellipsized width
+                null, // ellipsize
+                1,  // maxLines
+                BREAK_STRATEGY_SIMPLE,
+                HYPHENATION_FREQUENCY_NONE,
+                null,  // leftIndents
+                null,  // rightIndents
+                JUSTIFICATION_MODE_NONE,
+                null  // lineBreakConfig
+        );
+
+        mColumns = COLUMNS_ELLIPSIZE;
+        mLineDirections = ArrayUtils.newUnpaddedArray(Directions.class, 2);
+        mLines  = ArrayUtils.newUnpaddedIntArray(2 * mColumns);
+    }
+
+    /**
      * @deprecated Use {@link Builder} instead.
      */
     @Deprecated
diff --git a/core/java/android/view/InsetsController.java b/core/java/android/view/InsetsController.java
index 4ecfc40..c6d8bd1 100644
--- a/core/java/android/view/InsetsController.java
+++ b/core/java/android/view/InsetsController.java
@@ -16,10 +16,12 @@
 
 package android.view;
 
+import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR;
 import static android.os.Trace.TRACE_TAG_VIEW;
 import static android.view.InsetsControllerProto.CONTROL;
 import static android.view.InsetsControllerProto.STATE;
 import static android.view.InsetsSource.ID_IME;
+import static android.view.InsetsSource.ID_IME_CAPTION_BAR;
 import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
 import static android.view.WindowInsets.Type.FIRST;
 import static android.view.WindowInsets.Type.LAST;
@@ -40,6 +42,7 @@
 import android.content.Context;
 import android.content.res.CompatibilityInfo;
 import android.graphics.Insets;
+import android.graphics.Point;
 import android.graphics.Rect;
 import android.os.CancellationSignal;
 import android.os.Handler;
@@ -652,6 +655,7 @@
     private int mLastWindowingMode;
     private boolean mStartingAnimation;
     private int mCaptionInsetsHeight = 0;
+    private int mImeCaptionBarInsetsHeight = 0;
     private boolean mAnimationsDisabled;
     private boolean mCompatSysUiVisibilityStaled;
 
@@ -693,6 +697,9 @@
                     if (!CAPTION_ON_SHELL && source1.getType() == captionBar()) {
                         return;
                     }
+                    if (source1.getId() == ID_IME_CAPTION_BAR) {
+                        return;
+                    }
 
                     // Don't change the indexes of the sources while traversing. Remove it later.
                     mPendingRemoveIndexes.add(index1);
@@ -823,6 +830,9 @@
         if (mFrame.equals(frame)) {
             return;
         }
+        if (mImeCaptionBarInsetsHeight != 0) {
+            setImeCaptionBarInsetsHeight(mImeCaptionBarInsetsHeight);
+        }
         mHost.notifyInsetsChanged();
         mFrame.set(frame);
     }
@@ -1007,6 +1017,12 @@
         // Ensure to update all existing source consumers
         for (int i = mSourceConsumers.size() - 1; i >= 0; i--) {
             final InsetsSourceConsumer consumer = mSourceConsumers.valueAt(i);
+            if (consumer.getId() == ID_IME_CAPTION_BAR) {
+                // The inset control for the IME caption bar will never be dispatched
+                // by the server.
+                continue;
+            }
+
             final InsetsSourceControl control = mTmpControlArray.get(consumer.getId());
             if (control != null) {
                 controllableTypes |= control.getType();
@@ -1499,7 +1515,8 @@
                 continue;
             }
             final InsetsSourceControl control = consumer.getControl();
-            if (control != null && control.getLeash() != null) {
+            if (control != null
+                    && (control.getLeash() != null || control.getId() == ID_IME_CAPTION_BAR)) {
                 controls.put(control.getId(), new InsetsSourceControl(control));
                 typesReady |= consumer.getType();
             }
@@ -1885,6 +1902,35 @@
     }
 
     @Override
+    public void setImeCaptionBarInsetsHeight(int height) {
+        if (!ENABLE_HIDE_IME_CAPTION_BAR) {
+            return;
+        }
+        Rect newFrame = new Rect(mFrame.left, mFrame.bottom - height, mFrame.right, mFrame.bottom);
+        InsetsSource source = mState.peekSource(ID_IME_CAPTION_BAR);
+        if (mImeCaptionBarInsetsHeight != height
+                || (source != null && !newFrame.equals(source.getFrame()))) {
+            mImeCaptionBarInsetsHeight = height;
+            if (mImeCaptionBarInsetsHeight != 0) {
+                mState.getOrCreateSource(ID_IME_CAPTION_BAR, captionBar())
+                        .setFrame(newFrame);
+                getSourceConsumer(ID_IME_CAPTION_BAR, captionBar()).setControl(
+                        new InsetsSourceControl(ID_IME_CAPTION_BAR, captionBar(),
+                                null /* leash */, false /* initialVisible */,
+                                new Point(), Insets.NONE),
+                        new int[1], new int[1]);
+            } else {
+                mState.removeSource(ID_IME_CAPTION_BAR);
+                InsetsSourceConsumer sourceConsumer = mSourceConsumers.get(ID_IME_CAPTION_BAR);
+                if (sourceConsumer != null) {
+                    sourceConsumer.setControl(null, new int[1], new int[1]);
+                }
+            }
+            mHost.notifyInsetsChanged();
+        }
+    }
+
+    @Override
     public void setSystemBarsBehavior(@Behavior int behavior) {
         mHost.setSystemBarsBehavior(behavior);
     }
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index 6441186..ff009ed 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -20,6 +20,7 @@
 import static android.view.InsetsSourceProto.TYPE;
 import static android.view.InsetsSourceProto.VISIBLE;
 import static android.view.InsetsSourceProto.VISIBLE_FRAME;
+import static android.view.WindowInsets.Type.captionBar;
 import static android.view.WindowInsets.Type.ime;
 
 import android.annotation.IntDef;
@@ -47,6 +48,9 @@
 
     /** The insets source ID of IME */
     public static final int ID_IME = createId(null, 0, ime());
+    /** The insets source ID of the IME caption bar ("fake" IME navigation bar). */
+    static final int ID_IME_CAPTION_BAR =
+            InsetsSource.createId(null /* owner */, 1 /* index */, captionBar());
 
     /**
      * Controls whether this source suppresses the scrim. If the scrim is ignored, the system won't
@@ -215,8 +219,12 @@
         // During drag-move and drag-resizing, the caption insets position may not get updated
         // before the app frame get updated. To layout the app content correctly during drag events,
         // we always return the insets with the corresponding height covering the top.
+        // However, with the "fake" IME navigation bar treated as a caption bar, we return the
+        // insets with the corresponding height the bottom.
         if (getType() == WindowInsets.Type.captionBar()) {
-            return Insets.of(0, frame.height(), 0, 0);
+            return getId() == ID_IME_CAPTION_BAR
+                    ? Insets.of(0, 0, 0, frame.height())
+                    : Insets.of(0, frame.height(), 0, 0);
         }
         // Checks for whether there is shared edge with insets for 0-width/height window.
         final boolean hasIntersection = relativeFrame.isEmpty()
diff --git a/core/java/android/view/PendingInsetsController.java b/core/java/android/view/PendingInsetsController.java
index e8f62fc..a4cbc52 100644
--- a/core/java/android/view/PendingInsetsController.java
+++ b/core/java/android/view/PendingInsetsController.java
@@ -44,6 +44,7 @@
     private ArrayList<OnControllableInsetsChangedListener> mControllableInsetsChangedListeners
             = new ArrayList<>();
     private int mCaptionInsetsHeight = 0;
+    private int mImeCaptionBarInsetsHeight = 0;
     private WindowInsetsAnimationControlListener mLoggingListener;
     private @InsetsType int mRequestedVisibleTypes = WindowInsets.Type.defaultVisible();
 
@@ -91,6 +92,11 @@
     }
 
     @Override
+    public void setImeCaptionBarInsetsHeight(int height) {
+        mImeCaptionBarInsetsHeight = height;
+    }
+
+    @Override
     public void setSystemBarsBehavior(int behavior) {
         if (mReplayedInsetsController != null) {
             mReplayedInsetsController.setSystemBarsBehavior(behavior);
@@ -168,6 +174,9 @@
         if (mCaptionInsetsHeight != 0) {
             controller.setCaptionInsetsHeight(mCaptionInsetsHeight);
         }
+        if (mImeCaptionBarInsetsHeight != 0) {
+            controller.setImeCaptionBarInsetsHeight(mImeCaptionBarInsetsHeight);
+        }
         if (mAnimationsDisabled) {
             controller.setAnimationsDisabled(true);
         }
diff --git a/core/java/android/view/WindowInsetsController.java b/core/java/android/view/WindowInsetsController.java
index bc0bab7..cc2cd79 100644
--- a/core/java/android/view/WindowInsetsController.java
+++ b/core/java/android/view/WindowInsetsController.java
@@ -250,6 +250,16 @@
     void setCaptionInsetsHeight(int height);
 
     /**
+     * Sets the insets height for the IME caption bar, which corresponds to the
+     * "fake" IME navigation bar.
+     *
+     * @param height the insets height of the IME caption bar.
+     * @hide
+     */
+    default void setImeCaptionBarInsetsHeight(int height) {
+    }
+
+    /**
      * Controls the behavior of system bars.
      *
      * @param behavior Determines how the bars behave when being hidden by the application.
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 4614dce..a78b040 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -5041,6 +5041,21 @@
     </array>
 
     <!-- See DisplayWhiteBalanceController.
+         A float array containing a list of ambient brightnesses, in Lux. This array,
+         together with config_displayWhiteBalanceLowLightAmbientBiasesStrong, is used to generate a
+         lookup table used in DisplayWhiteBalanceController. This lookup table is used to map
+         ambient brightness readings to a bias, where the bias is used to linearly interpolate
+         between ambient color temperature and
+         config_displayWhiteBalanceLowLightAmbientColorTemperatureIdle.
+         This table is optional. If used, this array must,
+         1) Contain at least two entries
+         2) Be the same length as config_displayWhiteBalanceLowLightAmbientBiasesStrong. -->
+    <array name ="config_displayWhiteBalanceLowLightAmbientBrightnessesStrong">
+        <item>10.0</item>
+        <item>10.0</item>
+    </array>
+
+    <!-- See DisplayWhiteBalanceController.
          An array containing a list of biases. See
          config_displayWhiteBalanceLowLightAmbientBrightnesses for additional details.
          This array must be in the range of [0.0, 1.0]. -->
@@ -5050,12 +5065,28 @@
     </array>
 
     <!-- See DisplayWhiteBalanceController.
+         An array containing a list of biases. See
+         config_displayWhiteBalanceLowLightAmbientBrightnessesStrong for additional details.
+         This array must be in the range of [0.0, 1.0]. -->
+    <array name ="config_displayWhiteBalanceLowLightAmbientBiasesStrong">
+        <item>0.0</item>
+        <item>1.0</item>
+    </array>
+
+    <!-- See DisplayWhiteBalanceController.
          The ambient color temperature (in cct) to which we interpolate towards using the
          the look up table generated by config_displayWhiteBalanceLowLightAmbientBrightnesses
          and config_displayWhiteBalanceLowLightAmbientBiases. -->
     <item name="config_displayWhiteBalanceLowLightAmbientColorTemperature" format="float" type="dimen">6500.0</item>
 
     <!-- See DisplayWhiteBalanceController.
+         The ambient color temperature (in cct) to which we interpolate towards using the
+         the look up table generated by config_displayWhiteBalanceLowLightAmbientBrightnessesStrong
+         and config_displayWhiteBalanceLowLightAmbientBiasesStrong. Used when device is in Idle Screen
+         Brightness mode. -->
+    <item name="config_displayWhiteBalanceLowLightAmbientColorTemperatureStrong" format="float" type="dimen">6500.0</item>
+
+    <!-- See DisplayWhiteBalanceController.
          A float array containing a list of ambient brightnesses, in Lux. This array,
          together with config_displayWhiteBalanceHighLightAmbientBiases, is used to generate a
          lookup table used in DisplayWhiteBalanceController. This lookup table is used to map
@@ -5069,6 +5100,19 @@
     </array>
 
     <!-- See DisplayWhiteBalanceController.
+         A float array containing a list of ambient brightnesses, in Lux. This array,
+         together with config_displayWhiteBalanceHighLightAmbientBiasesStrong, is used to generate a
+         lookup table used in DisplayWhiteBalanceController. This lookup table is used to map
+         ambient brightness readings to a bias, where the bias is used to linearly interpolate
+         between ambient color temperature and
+         config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong.
+         This table is optional. If used, this array must,
+         1) Contain at least two entries
+         2) Be the same length as config_displayWhiteBalanceHighLightAmbientBiasesStrong. -->
+    <array name ="config_displayWhiteBalanceHighLightAmbientBrightnessesStrong">
+    </array>
+
+    <!-- See DisplayWhiteBalanceController.
          An array containing a list of biases. See
          config_displayWhiteBalanceHighLightAmbientBrightnesses for additional details.
          This array must be in the range of [0.0, 1.0]. -->
@@ -5076,12 +5120,26 @@
     </array>
 
     <!-- See DisplayWhiteBalanceController.
+         An array containing a list of biases. See
+         config_displayWhiteBalanceHighLightAmbientBrightnessesStrong for additional details.
+         This array must be in the range of [0.0, 1.0]. -->
+    <array name ="config_displayWhiteBalanceHighLightAmbientBiasesStrong">
+    </array>
+
+    <!-- See DisplayWhiteBalanceController.
          The ambient color temperature (in cct) to which we interpolate towards using the
          the look up table generated by config_displayWhiteBalanceHighLightAmbientBrightnesses
          and config_displayWhiteBalanceHighLightAmbientBiases. -->
     <item name="config_displayWhiteBalanceHighLightAmbientColorTemperature" format="float" type="dimen">8000.0</item>
 
     <!-- See DisplayWhiteBalanceController.
+         The ambient color temperature (in cct) to which we interpolate towards using the
+         the look up table generated by config_displayWhiteBalanceHighLightAmbientBrightnessesStrong
+         and config_displayWhiteBalanceHighLightAmbientBiasesStrong. Used when device is in Idle
+         Screen Brightness mode. -->
+    <item name="config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong" format="float" type="dimen">8000.0</item>
+
+    <!-- See DisplayWhiteBalanceController.
          A float array containing a list of ambient color temperatures, in Kelvin. This array,
          together with config_displayWhiteBalanceDisplayColorTemperatures, is used to generate a
          lookup table used in DisplayWhiteBalanceController. This lookup table is used to map
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index c5aa8b0..0dd6c74 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -1895,22 +1895,22 @@
     <!-- Content description which should be used for the fingerprint icon. -->
     <string name="fingerprint_icon_content_description">Fingerprint icon</string>
 
+    <!-- Notification name shown when the system requires the user to set up device unlock. [CHAR LIMIT=NONE] -->
+    <string name="device_unlock_notification_name">Device unlock</string>
+    <!-- Notification title shown when the system suggests the user to set up another way to unlock. [CHAR LIMIT=NONE] -->
+    <string name="alternative_unlock_setup_notification_title">Try another way to unlock</string>
+    <!-- Notification content shown when the system suggests the user to enroll their face. [CHAR LIMIT=NONE] -->
+    <string name="alternative_face_setup_notification_content">Use Face Unlock when your fingerprint isn\'t recognized, like when your fingers are wet</string>
+    <!-- Notification content shown when the system suggests the user to enroll their fingerprint. [CHAR LIMIT=NONE] -->
+    <string name="alternative_fp_setup_notification_content">Use Fingerprint Unlock when your face isn\'t recognized, like when there\'s not enough light</string>
     <!-- Notification name shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] -->
     <string name="face_recalibrate_notification_name">Face Unlock</string>
     <!-- Notification title shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] -->
     <string name="face_recalibrate_notification_title">Issue with Face Unlock</string>
     <!-- Notification content shown when the system requires the user to re-enroll their face. [CHAR LIMIT=NONE] -->
     <string name="face_recalibrate_notification_content">Tap to delete your face model, then add your face again</string>
-    <!-- Title of a notification that directs the user to set up Face Unlock by enrolling their face. [CHAR LIMIT=NONE] -->
-    <string name="face_setup_notification_title">Set up Face Unlock</string>
-    <!-- Contents of a notification that directs the user to set up face unlock by enrolling their face. [CHAR LIMIT=NONE] -->
-    <string name="face_setup_notification_content">Unlock your phone by looking at it</string>
     <!-- Error message indicating that the camera privacy sensor has been turned on [CHAR LIMIT=NONE] -->
     <string name="face_sensor_privacy_enabled">To use Face Unlock, turn on <b>Camera access</b> in Settings > Privacy</string>
-    <!-- Title of a notification that directs the user to enroll a fingerprint. [CHAR LIMIT=NONE] -->
-    <string name="fingerprint_setup_notification_title">Set up more ways to unlock</string>
-    <!-- Contents of a notification that directs the user to enroll a fingerprint. [CHAR LIMIT=NONE] -->
-    <string name="fingerprint_setup_notification_content">Tap to add a fingerprint</string>
 
     <!-- Notification name shown when the system requires the user to re-calibrate their fingerprint. [CHAR LIMIT=NONE] -->
     <string name="fingerprint_recalibrate_notification_name">Fingerprint Unlock</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 784e235..eaccd43 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2597,6 +2597,12 @@
   <!-- Biometric FRR config -->
   <java-symbol type="fraction" name="config_biometricNotificationFrrThreshold" />
 
+  <!-- Biometric FRR notification messages -->
+  <java-symbol type="string" name="device_unlock_notification_name" />
+  <java-symbol type="string" name="alternative_unlock_setup_notification_title" />
+  <java-symbol type="string" name="alternative_face_setup_notification_content" />
+  <java-symbol type="string" name="alternative_fp_setup_notification_content" />
+
   <!-- Device credential strings for BiometricManager -->
   <java-symbol type="string" name="screen_lock_app_setting_name" />
   <java-symbol type="string" name="screen_lock_dialog_default_subtitle" />
@@ -2637,8 +2643,6 @@
   <java-symbol type="string" name="fingerprint_recalibrate_notification_name" />
   <java-symbol type="string" name="fingerprint_recalibrate_notification_title" />
   <java-symbol type="string" name="fingerprint_recalibrate_notification_content" />
-  <java-symbol type="string" name="fingerprint_setup_notification_title" />
-  <java-symbol type="string" name="fingerprint_setup_notification_content" />
   <java-symbol type="string" name="fingerprint_error_power_pressed" />
 
   <!-- Fingerprint config -->
@@ -2706,7 +2710,6 @@
   <java-symbol type="string" name="face_authenticated_no_confirmation_required" />
   <java-symbol type="string" name="face_authenticated_confirmation_required" />
   <java-symbol type="string" name="face_error_security_update_required" />
-  <java-symbol type="string" name="face_setup_notification_title" />
 
   <java-symbol type="string" name="config_biometric_prompt_ui_package" />
   <java-symbol type="array" name="config_biometric_sensors" />
@@ -4170,11 +4173,17 @@
   <java-symbol type="array" name="config_displayWhiteBalanceIncreaseThresholds" />
   <java-symbol type="array" name="config_displayWhiteBalanceDecreaseThresholds" />
   <java-symbol type="array" name="config_displayWhiteBalanceLowLightAmbientBrightnesses" />
+  <java-symbol type="array" name="config_displayWhiteBalanceLowLightAmbientBrightnessesStrong" />
   <java-symbol type="array" name="config_displayWhiteBalanceLowLightAmbientBiases" />
+  <java-symbol type="array" name="config_displayWhiteBalanceLowLightAmbientBiasesStrong" />
   <java-symbol type="dimen" name="config_displayWhiteBalanceLowLightAmbientColorTemperature" />
+  <java-symbol type="dimen" name="config_displayWhiteBalanceLowLightAmbientColorTemperatureStrong" />
   <java-symbol type="array" name="config_displayWhiteBalanceHighLightAmbientBrightnesses" />
+  <java-symbol type="array" name="config_displayWhiteBalanceHighLightAmbientBrightnessesStrong" />
   <java-symbol type="array" name="config_displayWhiteBalanceHighLightAmbientBiases" />
+  <java-symbol type="array" name="config_displayWhiteBalanceHighLightAmbientBiasesStrong" />
   <java-symbol type="dimen" name="config_displayWhiteBalanceHighLightAmbientColorTemperature" />
+  <java-symbol type="dimen" name="config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong" />
   <java-symbol type="array" name="config_displayWhiteBalanceAmbientColorTemperatures" />
   <java-symbol type="array" name="config_displayWhiteBalanceDisplayColorTemperatures" />
   <java-symbol type="array" name="config_displayWhiteBalanceStrongAmbientColorTemperatures" />
diff --git a/graphics/java/android/graphics/fonts/SystemFonts.java b/graphics/java/android/graphics/fonts/SystemFonts.java
index 98629a2..36bfb98 100644
--- a/graphics/java/android/graphics/fonts/SystemFonts.java
+++ b/graphics/java/android/graphics/fonts/SystemFonts.java
@@ -235,6 +235,22 @@
     }
 
     /**
+     * Get the updated FontConfig.
+     *
+     * @param updatableFontMap a font mapping of updated font files.
+     * @hide
+     */
+    public static @NonNull FontConfig getSystemFontConfigForTesting(
+            @NonNull String fontsXml,
+            @Nullable Map<String, File> updatableFontMap,
+            long lastModifiedDate,
+            int configVersion
+    ) {
+        return getSystemFontConfigInternal(fontsXml, SYSTEM_FONT_DIR, OEM_XML, OEM_FONT_DIR,
+                updatableFontMap, lastModifiedDate, configVersion);
+    }
+
+    /**
      * Get the system preinstalled FontConfig.
      * @hide
      */
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
index ef062df..4b10b56 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperTest.java
@@ -23,14 +23,16 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.content.ContentProvider;
-import android.content.ContentResolver;
 import android.content.ContentValues;
 import android.content.Context;
 import android.content.pm.ApplicationInfo;
+import android.content.res.AssetFileDescriptor;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.database.MatrixCursor;
@@ -44,8 +46,8 @@
 import android.telephony.TelephonyManager;
 import android.test.mock.MockContentProvider;
 import android.test.mock.MockContentResolver;
+import android.util.ArrayMap;
 
-import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.internal.R;
@@ -77,13 +79,14 @@
 
     @Mock private Context mContext;
     @Mock private Resources mResources;
-    @Mock private ContentResolver mContentResolver;
     @Mock private AudioManager mAudioManager;
     @Mock private TelephonyManager mTelephonyManager;
 
+    @Mock private MockContentResolver mContentResolver;
+    private MockSettingsProvider mSettingsProvider;
+
     @Before
     public void setUp() {
-        clearLongPressPowerValues();
         MockitoAnnotations.initMocks(this);
         when(mContext.getSystemService(eq(Context.AUDIO_SERVICE))).thenReturn(mAudioManager);
         when(mContext.getSystemService(eq(Context.TELEPHONY_SERVICE))).thenReturn(
@@ -91,14 +94,20 @@
         when(mContext.getResources()).thenReturn(mResources);
         when(mContext.getApplicationContext()).thenReturn(mContext);
         when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
-        when(mContext.getContentResolver()).thenReturn(getContentResolver());
 
         mSettingsHelper = spy(new SettingsHelper(mContext));
+        mContentResolver = spy(new MockContentResolver());
+        when(mContext.getContentResolver()).thenReturn(mContentResolver);
+        mSettingsProvider = new MockSettingsProvider(mContext);
+        mContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider);
     }
 
     @After
     public void tearDown() {
-        clearLongPressPowerValues();
+        Settings.Global.putString(mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS,
+                null);
+        Settings.Global.putString(mContentResolver, Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
+                null);
     }
 
     @Test
@@ -123,33 +132,30 @@
         mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY,
                 SETTING_KEY, SETTING_VALUE, /* restoredFromSdkInt */ 0);
 
-        verifyZeroInteractions(mContentResolver);
+        // The only time of interaction happened during setUp()
+        verify(mContentResolver, times(1))
+                .addProvider(Settings.AUTHORITY, mSettingsProvider);
+
+        verifyNoMoreInteractions(mContentResolver);
     }
 
     @Test
     public void testRestoreValue_lppForAssistantEnabled_updatesValue() {
-        ContentResolver cr =
-                InstrumentationRegistry.getInstrumentation().getTargetContext()
-                        .getContentResolver();
         when(mResources.getBoolean(
                 R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn(
                 true);
 
-        mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY,
+        mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY,
                 Settings.Global.POWER_BUTTON_LONG_PRESS, "5", 0);
 
-        assertThat(
-                Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, -1))
-                    .isEqualTo(5);
-        assertThat(Settings.Global.getInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
-                -1)).isEqualTo(2);
+        assertThat(Settings.Global.getInt(
+                mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1)).isEqualTo(5);
+        assertThat(Settings.Global.getInt(
+                mContentResolver, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, -1)).isEqualTo(2);
     }
 
     @Test
     public void testRestoreValue_lppForAssistantNotEnabled_updatesValueToDefaultConfig() {
-        ContentResolver cr =
-                InstrumentationRegistry.getInstrumentation().getTargetContext()
-                        .getContentResolver();
         when(mResources.getBoolean(
                 R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn(
                 true);
@@ -161,21 +167,17 @@
                 R.integer.config_keyChordPowerVolumeUp)).thenReturn(
                 1);
 
-        mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY,
+        mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY,
                 Settings.Global.POWER_BUTTON_LONG_PRESS, "2", 0);
 
-        assertThat(
-                Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, -1))
-                .isEqualTo(1);
-        assertThat(Settings.Global.getInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
-                -1)).isEqualTo(1);
+        assertThat(Settings.Global.getInt(
+                mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1)).isEqualTo(1);
+        assertThat(Settings.Global.getInt(
+                mContentResolver, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, -1)).isEqualTo(1);
     }
 
     @Test
     public void testRestoreValue_lppForAssistantNotEnabledDefaultConfig_updatesValue() {
-        ContentResolver cr =
-                InstrumentationRegistry.getInstrumentation().getTargetContext()
-                        .getContentResolver();
         when(mResources.getBoolean(
                 R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn(
                 true);
@@ -187,47 +189,39 @@
                 R.integer.config_keyChordPowerVolumeUp)).thenReturn(
                 1);
 
-        mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY,
+        mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY,
                 Settings.Global.POWER_BUTTON_LONG_PRESS, "2", 0);
 
-        assertThat(
-                Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, -1))
-                    .isEqualTo(1);
-        assertThat(Settings.Global.getInt(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP,
-                -1)).isEqualTo(1);
+        assertThat(Settings.Global.getInt(
+                mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1)).isEqualTo(1);
+        assertThat(Settings.Global.getInt(
+                mContentResolver, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, -1)).isEqualTo(1);
     }
 
     @Test
     public void testRestoreValue_lppForAssistantNotAvailable_doesNotRestore() {
-        ContentResolver cr =
-                InstrumentationRegistry.getInstrumentation().getTargetContext()
-                        .getContentResolver();
-        when(mResources.getBoolean(
-                R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn(
-                false);
+        when(mResources.getBoolean(R.bool.config_longPressOnPowerForAssistantSettingAvailable))
+                .thenReturn(false);
 
-        mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY,
-                Settings.Global.POWER_BUTTON_LONG_PRESS, "5", 0);
+        mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY,
+                Settings.Global.POWER_BUTTON_LONG_PRESS, "500", 0);
 
-        assertThat((Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS,
-                -1))).isEqualTo(-1);
+        assertThat((Settings.Global.getInt(
+                mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1))).isEqualTo(-1);
     }
 
 
     @Test
     public void testRestoreValue_lppForAssistantInvalid_doesNotRestore() {
-        ContentResolver cr =
-                InstrumentationRegistry.getInstrumentation().getTargetContext()
-                        .getContentResolver();
         when(mResources.getBoolean(
                 R.bool.config_longPressOnPowerForAssistantSettingAvailable)).thenReturn(
                 false);
 
-        mSettingsHelper.restoreValue(mContext, cr, new ContentValues(), Uri.EMPTY,
+        mSettingsHelper.restoreValue(mContext, mContentResolver, new ContentValues(), Uri.EMPTY,
                 Settings.Global.POWER_BUTTON_LONG_PRESS, "trees", 0);
 
-        assertThat((Settings.Global.getInt(cr, Settings.Global.POWER_BUTTON_LONG_PRESS,
-                -1))).isEqualTo(-1);
+        assertThat((Settings.Global.getInt(
+                mContentResolver, Settings.Global.POWER_BUTTON_LONG_PRESS, -1))).isEqualTo(-1);
     }
 
     @Test
@@ -363,9 +357,6 @@
         final String newRingtoneValueCanonicalized =
                 "content://media/internal/audio/media/100?title=Song&canonical=1";
 
-        MockContentResolver mMockContentResolver = new MockContentResolver();
-        when(mContext.getContentResolver()).thenReturn(mMockContentResolver);
-
         ContentProvider mockMediaContentProvider =
                 new MockContentProvider(mContext) {
                     @Override
@@ -386,25 +377,22 @@
                     }
                 };
 
-        ContentProvider mockSettingsContentProvider =
-                new MockSettingsProvider(mContext, getContentResolver());
-        mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider);
+        mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
 
-        resetRingtoneSettingsToDefault(mMockContentResolver);
-        assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE))
+        resetRingtoneSettingsToDefault();
+        assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE))
                 .isEqualTo(DEFAULT_RINGTONE_VALUE);
 
         mSettingsHelper.restoreValue(
                 mContext,
-                mMockContentResolver,
+                mContentResolver,
                 new ContentValues(),
                 Uri.EMPTY,
                 Settings.System.RINGTONE,
                 sourceRingtoneValue,
                 0);
 
-        assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE))
+        assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE))
                 .isEqualTo(newRingtoneValueCanonicalized);
     }
 
@@ -417,9 +405,6 @@
         final String newRingtoneValueCanonicalized =
                 "content://0@media/external/audio/media/100?title=Song&canonical=1";
 
-        MockContentResolver mMockContentResolver = new MockContentResolver();
-        when(mContext.getContentResolver()).thenReturn(mMockContentResolver);
-
         MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID});
         cursor.addRow(new Object[] {100L});
 
@@ -458,24 +443,21 @@
                     }
                 };
 
-        ContentProvider mockSettingsContentProvider =
-                new MockSettingsProvider(mContext, getContentResolver());
-        mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider);
+        mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
+        mContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider);
 
-        resetRingtoneSettingsToDefault(mMockContentResolver);
+        resetRingtoneSettingsToDefault();
 
         mSettingsHelper.restoreValue(
                 mContext,
-                mMockContentResolver,
+                mContentResolver,
                 new ContentValues(),
                 Uri.EMPTY,
                 Settings.System.RINGTONE,
                 sourceRingtoneValue,
                 0);
 
-        assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE))
+        assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE))
                 .isEqualTo(newRingtoneValueCanonicalized);
     }
 
@@ -488,9 +470,6 @@
         final String newRingtoneValueCanonicalized =
                 "content://0@media/external/audio/media/200?title=notificationPing&canonicalize=1";
 
-        MockContentResolver mMockContentResolver = new MockContentResolver();
-        when(mContext.getContentResolver()).thenReturn(mMockContentResolver);
-
         MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID});
         cursor.addRow(new Object[] {200L});
 
@@ -529,17 +508,14 @@
                     }
                 };
 
-        ContentProvider mockSettingsContentProvider =
-                new MockSettingsProvider(mContext, getContentResolver());
-        mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider);
+        mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
+        mContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider);
 
-        resetRingtoneSettingsToDefault(mMockContentResolver);
+        resetRingtoneSettingsToDefault();
 
         mSettingsHelper.restoreValue(
                 mContext,
-                mMockContentResolver,
+                mContentResolver,
                 new ContentValues(),
                 Uri.EMPTY,
                 Settings.System.NOTIFICATION_SOUND,
@@ -548,7 +524,7 @@
 
         assertThat(
                         Settings.System.getString(
-                                mMockContentResolver, Settings.System.NOTIFICATION_SOUND))
+                                mContentResolver, Settings.System.NOTIFICATION_SOUND))
                 .isEqualTo(newRingtoneValueCanonicalized);
     }
 
@@ -561,9 +537,6 @@
         final String newRingtoneValueCanonicalized =
                 "content://0@media/external/audio/media/300?title=alarmSound&canonical=1";
 
-        MockContentResolver mMockContentResolver = new MockContentResolver();
-        when(mContext.getContentResolver()).thenReturn(mMockContentResolver);
-
         MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID});
         cursor.addRow(new Object[] {300L});
 
@@ -600,26 +573,29 @@
                         assertThat(selectionArgs).isEqualTo(new String[] {"alarmSound"});
                         return cursor;
                     }
+
+                    @Override
+                    public AssetFileDescriptor openTypedAssetFile(Uri url, String mimeType,
+                            Bundle opts) {
+                        return null;
+                    }
                 };
 
-        ContentProvider mockSettingsContentProvider =
-                new MockSettingsProvider(mContext, getContentResolver());
-        mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider);
+        mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
+        mContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider);
 
-        resetRingtoneSettingsToDefault(mMockContentResolver);
+        resetRingtoneSettingsToDefault();
 
         mSettingsHelper.restoreValue(
                 mContext,
-                mMockContentResolver,
+                mContentResolver,
                 new ContentValues(),
                 Uri.EMPTY,
                 Settings.System.ALARM_ALERT,
                 sourceRingtoneValue,
                 0);
 
-        assertThat(Settings.System.getString(mMockContentResolver, Settings.System.ALARM_ALERT))
+        assertThat(Settings.System.getString(mContentResolver, Settings.System.ALARM_ALERT))
                 .isEqualTo(newRingtoneValueCanonicalized);
     }
 
@@ -628,9 +604,6 @@
         final String sourceRingtoneValue =
                 "content://0@media/external/audio/media/1?title=Song&canonical=1";
 
-        MockContentResolver mMockContentResolver = new MockContentResolver();
-        when(mContext.getContentResolver()).thenReturn(mMockContentResolver);
-
         // This is to mock the case that there are multiple results by querying title +
         // ringtone_type.
         MatrixCursor cursor = new MatrixCursor(new String[] {BaseColumns._ID});
@@ -651,32 +624,26 @@
                     }
                 };
 
-        ContentProvider mockSettingsContentProvider =
-                new MockSettingsProvider(mContext, getContentResolver());
-        mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider);
+        mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
+        mContentResolver.addProvider("0@" + MediaStore.AUTHORITY, mockMediaContentProvider);
 
-        resetRingtoneSettingsToDefault(mMockContentResolver);
+        resetRingtoneSettingsToDefault();
 
         mSettingsHelper.restoreValue(
                 mContext,
-                mMockContentResolver,
+                mContentResolver,
                 new ContentValues(),
                 Uri.EMPTY,
                 Settings.System.RINGTONE,
                 sourceRingtoneValue,
                 0);
 
-        assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE))
+        assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE))
                 .isEqualTo(DEFAULT_RINGTONE_VALUE);
     }
 
     @Test
     public void testRestoreValue_customRingtone_restoreSilentValue() {
-        MockContentResolver mMockContentResolver = new MockContentResolver();
-        when(mContext.getContentResolver()).thenReturn(mMockContentResolver);
-
         ContentProvider mockMediaContentProvider =
                 new MockContentProvider(mContext) {
                     @Override
@@ -691,37 +658,46 @@
                     }
                 };
 
-        ContentProvider mockSettingsContentProvider =
-                new MockSettingsProvider(mContext, getContentResolver());
-        mMockContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
-        mMockContentResolver.addProvider(Settings.AUTHORITY, mockSettingsContentProvider);
+        mContentResolver.addProvider(MediaStore.AUTHORITY, mockMediaContentProvider);
 
-        resetRingtoneSettingsToDefault(mMockContentResolver);
+        resetRingtoneSettingsToDefault();
 
         mSettingsHelper.restoreValue(
                 mContext,
-                mMockContentResolver,
+                mContentResolver,
                 new ContentValues(),
                 Uri.EMPTY,
                 Settings.System.RINGTONE,
                 "_silent",
                 0);
 
-        assertThat(Settings.System.getString(mMockContentResolver, Settings.System.RINGTONE))
+        assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE))
                 .isEqualTo(null);
     }
 
-    public static class MockSettingsProvider extends MockContentProvider {
-        ContentResolver mBaseContentResolver;
-
-        public MockSettingsProvider(Context context, ContentResolver baseContentResolver) {
+    private static class MockSettingsProvider extends MockContentProvider {
+        private final ArrayMap<String, String> mKeyValueStore = new ArrayMap<>();
+        MockSettingsProvider(Context context) {
             super(context);
-            this.mBaseContentResolver = baseContentResolver;
         }
 
         @Override
         public Bundle call(String method, String request, Bundle args) {
-            return mBaseContentResolver.call(Settings.AUTHORITY, method, request, args);
+            if (method.startsWith("PUT_")) {
+                mKeyValueStore.put(request, args.getString("value"));
+                return null;
+            } else if (method.startsWith("GET_")) {
+                return Bundle.forPair("value", mKeyValueStore.getOrDefault(request, ""));
+            }
+            return null;
+        }
+
+        @Override
+        public Uri insert(Uri uri, ContentValues values) {
+            String name = values.getAsString("name");
+            String value = values.getAsString("value");
+            mKeyValueStore.put(name, value);
+            return null;
         }
     }
 
@@ -752,15 +728,13 @@
     }
 
     private int getAutoRotationSettingValue() {
-        return Settings.System.getInt(
-                getContentResolver(),
+        return Settings.System.getInt(mContentResolver,
                 Settings.System.ACCELEROMETER_ROTATION,
                 /* default= */ -1);
     }
 
     private void setAutoRotationSettingValue(int value) {
-        Settings.System.putInt(
-                getContentResolver(),
+        Settings.System.putInt(mContentResolver,
                 Settings.System.ACCELEROMETER_ROTATION,
                 value
         );
@@ -769,7 +743,7 @@
     private void restoreAutoRotationSetting(int newValue) {
         mSettingsHelper.restoreValue(
                 mContext,
-                getContentResolver(),
+                mContentResolver,
                 new ContentValues(),
                 /* destination= */ Settings.System.CONTENT_URI,
                 /* name= */ Settings.System.ACCELEROMETER_ROTATION,
@@ -777,31 +751,19 @@
                 /* restoredFromSdkInt= */ 0);
     }
 
-    private ContentResolver getContentResolver() {
-        return InstrumentationRegistry.getInstrumentation().getTargetContext()
-                .getContentResolver();
-    }
-
-    private void clearLongPressPowerValues() {
-        ContentResolver cr = InstrumentationRegistry.getInstrumentation().getTargetContext()
-                .getContentResolver();
-        Settings.Global.putString(cr, Settings.Global.POWER_BUTTON_LONG_PRESS, null);
-        Settings.Global.putString(cr, Settings.Global.KEY_CHORD_POWER_VOLUME_UP, null);
-    }
-
-    private void resetRingtoneSettingsToDefault(ContentResolver contentResolver) {
+    private void resetRingtoneSettingsToDefault() {
         Settings.System.putString(
-                contentResolver, Settings.System.RINGTONE, DEFAULT_RINGTONE_VALUE);
+                mContentResolver, Settings.System.RINGTONE, DEFAULT_RINGTONE_VALUE);
         Settings.System.putString(
-                contentResolver, Settings.System.NOTIFICATION_SOUND, DEFAULT_NOTIFICATION_VALUE);
+                mContentResolver, Settings.System.NOTIFICATION_SOUND, DEFAULT_NOTIFICATION_VALUE);
         Settings.System.putString(
-                contentResolver, Settings.System.ALARM_ALERT, DEFAULT_ALARM_VALUE);
+                mContentResolver, Settings.System.ALARM_ALERT, DEFAULT_ALARM_VALUE);
 
-        assertThat(Settings.System.getString(contentResolver, Settings.System.RINGTONE))
+        assertThat(Settings.System.getString(mContentResolver, Settings.System.RINGTONE))
                 .isEqualTo(DEFAULT_RINGTONE_VALUE);
-        assertThat(Settings.System.getString(contentResolver, Settings.System.NOTIFICATION_SOUND))
+        assertThat(Settings.System.getString(mContentResolver, Settings.System.NOTIFICATION_SOUND))
                 .isEqualTo(DEFAULT_NOTIFICATION_VALUE);
-        assertThat(Settings.System.getString(contentResolver, Settings.System.ALARM_ALERT))
+        assertThat(Settings.System.getString(mContentResolver, Settings.System.ALARM_ALERT))
                 .isEqualTo(DEFAULT_ALARM_VALUE);
     }
 }
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
index e539c95..e7a53e5b 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt
@@ -62,6 +62,7 @@
     private val burmeseLineSpacing =
         resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale_burmese)
     private val defaultLineSpacing = resources.getFloat(R.dimen.keyguard_clock_line_spacing_scale)
+    protected var onSecondaryDisplay: Boolean = false
 
     override val events: DefaultClockEvents
     override val config = ClockConfig()
@@ -142,6 +143,11 @@
                     view.setTextSize(TypedValue.COMPLEX_UNIT_PX, fontSizePx)
                     recomputePadding(targetRegion)
                 }
+
+                override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {
+                    this@DefaultClockController.onSecondaryDisplay = onSecondaryDisplay
+                    recomputePadding(null)
+                }
             }
 
         open fun recomputePadding(targetRegion: Rect?) {}
@@ -182,13 +188,19 @@
         override fun recomputePadding(targetRegion: Rect?) {
             // We center the view within the targetRegion instead of within the parent
             // view by computing the difference and adding that to the padding.
-            val parent = view.parent
-            val yDiff =
-                if (targetRegion != null && parent is View && parent.isLaidOut())
-                    targetRegion.centerY() - parent.height / 2f
-                else 0f
             val lp = view.getLayoutParams() as FrameLayout.LayoutParams
-            lp.topMargin = (-0.5f * view.bottom + yDiff).toInt()
+            lp.topMargin =
+                if (onSecondaryDisplay) {
+                    // On the secondary display we don't want any additional top/bottom margin.
+                    0
+                } else {
+                    val parent = view.parent
+                    val yDiff =
+                        if (targetRegion != null && parent is View && parent.isLaidOut())
+                            targetRegion.centerY() - parent.height / 2f
+                        else 0f
+                    (-0.5f * view.bottom + yDiff).toInt()
+                }
             view.setLayoutParams(lp)
         }
 
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index d962732..527f8007 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -177,6 +177,9 @@
      * targetRegion is relative to the parent view.
      */
     fun onTargetRegionChanged(targetRegion: Rect?)
+
+    /** Called to notify the clock about its display. */
+    fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean)
 }
 
 /** Tick rates for clocks */
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_presentation.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_presentation.xml
new file mode 100644
index 0000000..593f507f
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_presentation.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+**
+** Copyright 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.
+*/
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/presentation"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <com.android.keyguard.KeyguardStatusView
+        android:id="@+id/clock"
+        android:layout_width="410dp"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center"
+        android:orientation="vertical">
+
+        <include
+            android:id="@+id/keyguard_clock_container"
+            layout="@layout/keyguard_clock_switch"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content" />
+    </com.android.keyguard.KeyguardStatusView>
+
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/combined_qs_header.xml b/packages/SystemUI/res/layout/combined_qs_header.xml
index 12f13e9..3a15ae4 100644
--- a/packages/SystemUI/res/layout/combined_qs_header.xml
+++ b/packages/SystemUI/res/layout/combined_qs_header.xml
@@ -127,6 +127,8 @@
         android:gravity="center_vertical"
         android:paddingStart="@dimen/shade_header_system_icons_padding_start"
         android:paddingEnd="@dimen/shade_header_system_icons_padding_end"
+        android:paddingTop="@dimen/shade_header_system_icons_padding_top"
+        android:paddingBottom="@dimen/shade_header_system_icons_padding_bottom"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="@id/privacy_container"
         app:layout_constraintTop_toTopOf="@id/clock">
diff --git a/packages/SystemUI/res/layout/screen_share_dialog.xml b/packages/SystemUI/res/layout/screen_share_dialog.xml
index 9af46c5..3796415 100644
--- a/packages/SystemUI/res/layout/screen_share_dialog.xml
+++ b/packages/SystemUI/res/layout/screen_share_dialog.xml
@@ -64,8 +64,7 @@
             android:layout_height="wrap_content"
             android:text="@string/screenrecord_permission_dialog_warning_entire_screen"
             style="@style/TextAppearance.Dialog.Body.Message"
-            android:gravity="start"
-            android:lineHeight="@dimen/screenrecord_warning_line_height"/>
+            android:gravity="start"/>
 
         <!-- Buttons -->
         <LinearLayout
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index d85e012..915dcdb 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -82,6 +82,8 @@
     <!-- start padding is smaller to account for status icon margins coming from drawable itself -->
     <dimen name="shade_header_system_icons_padding_start">3dp</dimen>
     <dimen name="shade_header_system_icons_padding_end">4dp</dimen>
+    <dimen name="shade_header_system_icons_padding_top">2dp</dimen>
+    <dimen name="shade_header_system_icons_padding_bottom">2dp</dimen>
 
     <!-- Lockscreen shade transition values -->
     <dimen name="lockscreen_shade_transition_by_tap_distance">200dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 8310b95..2dc1b45 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -496,9 +496,10 @@
     <dimen name="large_screen_shade_header_min_height">@dimen/qs_header_row_min_height</dimen>
     <dimen name="large_screen_shade_header_left_padding">@dimen/qs_horizontal_margin</dimen>
     <dimen name="shade_header_system_icons_height">@dimen/large_screen_shade_header_min_height</dimen>
-    <dimen name="shade_header_system_icons_height_large_screen">32dp</dimen>
     <dimen name="shade_header_system_icons_padding_start">0dp</dimen>
     <dimen name="shade_header_system_icons_padding_end">0dp</dimen>
+    <dimen name="shade_header_system_icons_padding_top">0dp</dimen>
+    <dimen name="shade_header_system_icons_padding_bottom">0dp</dimen>
 
     <!-- The top margin of the panel that holds the list of notifications.
          On phones it's always 0dp but it's overridden in Car UI
diff --git a/packages/SystemUI/res/xml/large_screen_shade_header.xml b/packages/SystemUI/res/xml/large_screen_shade_header.xml
index 2ec6180..fe61c46 100644
--- a/packages/SystemUI/res/xml/large_screen_shade_header.xml
+++ b/packages/SystemUI/res/xml/large_screen_shade_header.xml
@@ -56,7 +56,7 @@
     <Constraint android:id="@+id/shade_header_system_icons">
         <Layout
             android:layout_width="wrap_content"
-            android:layout_height="@dimen/shade_header_system_icons_height_large_screen"
+            android:layout_height="wrap_content"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintEnd_toStartOf="@id/privacy_container"
             app:layout_constraintTop_toTopOf="parent"
diff --git a/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
new file mode 100644
index 0000000..899cad89
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/ConnectedDisplayKeyguardPresentation.kt
@@ -0,0 +1,85 @@
+/*
+ * 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.keyguard
+
+import android.app.Presentation
+import android.content.Context
+import android.graphics.Color
+import android.os.Bundle
+import android.view.Display
+import android.view.LayoutInflater
+import android.view.View
+import android.view.WindowManager
+import com.android.keyguard.dagger.KeyguardStatusViewComponent
+import com.android.systemui.R
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** [Presentation] shown in connected displays while on keyguard. */
+class ConnectedDisplayKeyguardPresentation
+@AssistedInject
+constructor(
+    @Assisted display: Display,
+    context: Context,
+    private val keyguardStatusViewComponentFactory: KeyguardStatusViewComponent.Factory,
+) :
+    Presentation(
+        context,
+        display,
+        R.style.Theme_SystemUI_KeyguardPresentation,
+        WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG
+    ) {
+
+    private lateinit var keyguardStatusViewController: KeyguardStatusViewController
+    private lateinit var clock: KeyguardStatusView
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        setContentView(
+            LayoutInflater.from(context)
+                .inflate(R.layout.keyguard_clock_presentation, /* root= */ null)
+        )
+        val window = window ?: error("no window available.")
+
+        // Logic to make the lock screen fullscreen
+        window.decorView.systemUiVisibility =
+            (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
+                View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
+                View.SYSTEM_UI_FLAG_LAYOUT_STABLE)
+        window.attributes.fitInsetsTypes = 0
+        window.isNavigationBarContrastEnforced = false
+        window.navigationBarColor = Color.TRANSPARENT
+
+        clock = findViewById(R.id.clock)
+        keyguardStatusViewController =
+            keyguardStatusViewComponentFactory.build(clock).keyguardStatusViewController.apply {
+                setDisplayedOnSecondaryDisplay()
+                init()
+            }
+    }
+
+    /** [ConnectedDisplayKeyguardPresentation] factory. */
+    @AssistedFactory
+    interface Factory {
+        /** Creates a new [Presentation] for the given [display]. */
+        fun create(
+            display: Display,
+        ): ConnectedDisplayKeyguardPresentation
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 30b8ed0..b589887 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -1,10 +1,5 @@
 package com.android.keyguard;
 
-import static android.view.View.ALPHA;
-import static android.view.View.SCALE_X;
-import static android.view.View.SCALE_Y;
-import static android.view.View.TRANSLATION_Y;
-
 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_X_CLOCK_DESIGN;
 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_DESIGN;
 import static com.android.keyguard.KeyguardStatusAreaView.TRANSLATE_Y_CLOCK_SIZE;
@@ -149,6 +144,13 @@
         updateStatusArea(/* animate= */false);
     }
 
+    /** Sets whether the large clock is being shown on a connected display. */
+    public void setLargeClockOnSecondaryDisplay(boolean onSecondaryDisplay) {
+        if (mClock != null) {
+            mClock.getLargeClock().getEvents().onSecondaryDisplayChanged(onSecondaryDisplay);
+        }
+    }
+
     /**
      * Enable or disable split shade specific positioning
      */
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 7a5ad5e..dd39f1d 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -104,6 +104,7 @@
 
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
 
+    private boolean mShownOnSecondaryDisplay = false;
     private boolean mOnlyClock = false;
     private boolean mIsActiveDreamLockscreenHosted = false;
     private FeatureFlags mFeatureFlags;
@@ -185,8 +186,18 @@
     }
 
     /**
-     * Mostly used for alternate displays, limit the information shown
+     * When set, limits the information shown in an external display.
      */
+    public void setShownOnSecondaryDisplay(boolean shownOnSecondaryDisplay) {
+        mShownOnSecondaryDisplay = shownOnSecondaryDisplay;
+    }
+
+    /**
+     * Mostly used for alternate displays, limit the information shown
+     *
+     * @deprecated use {@link KeyguardClockSwitchController#setShownOnSecondaryDisplay}
+     */
+    @Deprecated
     public void setOnlyClock(boolean onlyClock) {
         mOnlyClock = onlyClock;
     }
@@ -221,6 +232,15 @@
         }
     }
 
+    private void hideSliceViewAndNotificationIconContainer() {
+        View ksv = mView.findViewById(R.id.keyguard_slice_view);
+        ksv.setVisibility(View.GONE);
+
+        View nic = mView.findViewById(
+                R.id.left_aligned_notification_icon_container);
+        nic.setVisibility(View.GONE);
+    }
+
     @Override
     protected void onViewAttached() {
         mClockRegistry.registerClockChangeListener(mClockChangedListener);
@@ -234,13 +254,15 @@
         mKeyguardDateWeatherViewInvisibility =
                 mView.getResources().getInteger(R.integer.keyguard_date_weather_view_invisibility);
 
-        if (mOnlyClock) {
-            View ksv = mView.findViewById(R.id.keyguard_slice_view);
-            ksv.setVisibility(View.GONE);
+        if (mShownOnSecondaryDisplay) {
+            mView.setLargeClockOnSecondaryDisplay(true);
+            displayClock(LARGE, /* animate= */ false);
+            hideSliceViewAndNotificationIconContainer();
+            return;
+        }
 
-            View nic = mView.findViewById(
-                    R.id.left_aligned_notification_icon_container);
-            nic.setVisibility(View.GONE);
+        if (mOnlyClock) {
+            hideSliceViewAndNotificationIconContainer();
             return;
         }
         updateAodIcons();
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
index 9f21a31..1c5a575 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardDisplayManager.java
@@ -43,16 +43,19 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dagger.qualifiers.UiBackground;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.settings.DisplayTracker;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 
+import dagger.Lazy;
+
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
 
-import dagger.Lazy;
 
 @SysUISingleton
 public class KeyguardDisplayManager {
@@ -64,6 +67,9 @@
     private final DisplayTracker mDisplayTracker;
     private final Lazy<NavigationBarController> mNavigationBarControllerLazy;
     private final KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
+    private final ConnectedDisplayKeyguardPresentation.Factory
+            mConnectedDisplayKeyguardPresentationFactory;
+    private final FeatureFlags mFeatureFlags;
     private final Context mContext;
 
     private boolean mShowing;
@@ -105,7 +111,10 @@
             @Main Executor mainExecutor,
             @UiBackground Executor uiBgExecutor,
             DeviceStateHelper deviceStateHelper,
-            KeyguardStateController keyguardStateController) {
+            KeyguardStateController keyguardStateController,
+            ConnectedDisplayKeyguardPresentation.Factory
+                    connectedDisplayKeyguardPresentationFactory,
+            FeatureFlags featureFlags) {
         mContext = context;
         mNavigationBarControllerLazy = navigationBarControllerLazy;
         mKeyguardStatusViewComponentFactory = keyguardStatusViewComponentFactory;
@@ -115,6 +124,8 @@
         mDisplayTracker.addDisplayChangeCallback(mDisplayCallback, mainExecutor);
         mDeviceStateHelper = deviceStateHelper;
         mKeyguardStateController = keyguardStateController;
+        mConnectedDisplayKeyguardPresentationFactory = connectedDisplayKeyguardPresentationFactory;
+        mFeatureFlags = featureFlags;
     }
 
     private boolean isKeyguardShowable(Display display) {
@@ -185,8 +196,12 @@
         return false;
     }
 
-    KeyguardPresentation createPresentation(Display display) {
-        return new KeyguardPresentation(mContext, display, mKeyguardStatusViewComponentFactory);
+    Presentation createPresentation(Display display) {
+        if (mFeatureFlags.isEnabled(Flags.ENABLE_CLOCK_KEYGUARD_PRESENTATION)) {
+            return mConnectedDisplayKeyguardPresentationFactory.create(display);
+        } else {
+            return new KeyguardPresentation(mContext, display, mKeyguardStatusViewComponentFactory);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 757022d..c314586 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -187,6 +187,11 @@
         mConfigurationController.removeCallback(mConfigurationListener);
     }
 
+    /** Sets the StatusView as shown on an external display. */
+    public void setDisplayedOnSecondaryDisplay() {
+        mKeyguardClockSwitchController.setShownOnSecondaryDisplay(true);
+    }
+
     /**
      * Called in notificationPanelViewController to avoid leak
      */
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index cca71e8..f3900ac 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -115,7 +115,7 @@
     // TODO(b/292213543): Tracking Bug
     @JvmField
     val NOTIFICATION_GROUP_EXPANSION_CHANGE =
-            unreleasedFlag("notification_group_expansion_change", teamfood = false)
+            unreleasedFlag("notification_group_expansion_change", teamfood = true)
 
     // 200 - keyguard/lockscreen
     // ** Flag retired **
@@ -769,6 +769,10 @@
     @JvmField
     val ONE_WAY_HAPTICS_API_MIGRATION = unreleasedFlag("oneway_haptics_api_migration")
 
+    /** TODO(b/296223317): Enables the new keyguard presentation containing a clock. */
+    @JvmField
+    val ENABLE_CLOCK_KEYGUARD_PRESENTATION = unreleasedFlag("enable_clock_keyguard_presentation")
+
     /** Enable the Compose implementation of the PeopleSpaceActivity. */
     @JvmField
     val COMPOSE_PEOPLE_SPACE = unreleasedFlag("compose_people_space")
diff --git a/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt
new file mode 100644
index 0000000..3f2f67d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractor.kt
@@ -0,0 +1,67 @@
+/*
+ * 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.keyevent.domain.interactor
+
+import android.view.KeyEvent
+import com.android.systemui.back.domain.interactor.BackActionInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.KeyguardKeyEventInteractor
+import javax.inject.Inject
+
+/**
+ * Sends key events to the appropriate interactors and then acts upon key events that haven't
+ * already been handled but should be handled by SystemUI.
+ */
+@SysUISingleton
+class KeyEventInteractor
+@Inject
+constructor(
+    private val backActionInteractor: BackActionInteractor,
+    private val keyguardKeyEventInteractor: KeyguardKeyEventInteractor,
+) {
+    fun dispatchKeyEvent(event: KeyEvent): Boolean {
+        if (keyguardKeyEventInteractor.dispatchKeyEvent(event)) {
+            return true
+        }
+
+        when (event.keyCode) {
+            KeyEvent.KEYCODE_BACK -> {
+                if (event.handleAction()) {
+                    backActionInteractor.onBackRequested()
+                }
+                return true
+            }
+        }
+        return false
+    }
+
+    fun interceptMediaKey(event: KeyEvent): Boolean {
+        return keyguardKeyEventInteractor.interceptMediaKey(event)
+    }
+
+    fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
+        return keyguardKeyEventInteractor.dispatchKeyEventPreIme(event)
+    }
+
+    companion object {
+        // Most actions shouldn't be handled on the down event and instead handled on subsequent
+        // key events like ACTION_UP.
+        fun KeyEvent.handleAction(): Boolean {
+            return action != KeyEvent.ACTION_DOWN
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
new file mode 100644
index 0000000..635961b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractor.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import android.content.Context
+import android.media.AudioManager
+import android.view.KeyEvent
+import com.android.systemui.back.domain.interactor.BackActionInteractor
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor.Companion.handleAction
+import com.android.systemui.media.controls.util.MediaSessionLegacyHelperWrapper
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import javax.inject.Inject
+
+/** Handles key events arriving when the keyguard is showing or device is dozing. */
+@SysUISingleton
+class KeyguardKeyEventInteractor
+@Inject
+constructor(
+    private val context: Context,
+    private val statusBarStateController: StatusBarStateController,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
+    private val shadeController: ShadeController,
+    private val mediaSessionLegacyHelperWrapper: MediaSessionLegacyHelperWrapper,
+    private val backActionInteractor: BackActionInteractor,
+) {
+
+    fun dispatchKeyEvent(event: KeyEvent): Boolean {
+        if (statusBarStateController.isDozing) {
+            when (event.keyCode) {
+                KeyEvent.KEYCODE_VOLUME_DOWN,
+                KeyEvent.KEYCODE_VOLUME_UP -> return dispatchVolumeKeyEvent(event)
+            }
+        }
+
+        if (event.handleAction()) {
+            when (event.keyCode) {
+                KeyEvent.KEYCODE_MENU -> return dispatchMenuKeyEvent()
+                KeyEvent.KEYCODE_SPACE -> return dispatchSpaceEvent()
+            }
+        }
+        return false
+    }
+
+    /**
+     * While IME is active and a BACK event is detected, check with {@link
+     * StatusBarKeyguardViewManager#dispatchBackKeyEventPreIme()} to see if the event should be
+     * handled before routing to IME, in order to prevent the user from having to hit back twice to
+     * exit bouncer.
+     */
+    fun dispatchKeyEventPreIme(event: KeyEvent): Boolean {
+        when (event.keyCode) {
+            KeyEvent.KEYCODE_BACK ->
+                if (
+                    statusBarStateController.state == StatusBarState.KEYGUARD &&
+                        statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()
+                ) {
+                    return backActionInteractor.onBackRequested()
+                }
+        }
+        return false
+    }
+
+    fun interceptMediaKey(event: KeyEvent): Boolean {
+        return statusBarStateController.state == StatusBarState.KEYGUARD &&
+            statusBarKeyguardViewManager.interceptMediaKey(event)
+    }
+
+    private fun dispatchMenuKeyEvent(): Boolean {
+        val shouldUnlockOnMenuPressed =
+            isDeviceInteractive() &&
+                (statusBarStateController.state != StatusBarState.SHADE) &&
+                statusBarKeyguardViewManager.shouldDismissOnMenuPressed()
+        if (shouldUnlockOnMenuPressed) {
+            shadeController.animateCollapseShadeForced()
+            return true
+        }
+        return false
+    }
+
+    private fun dispatchSpaceEvent(): Boolean {
+        if (isDeviceInteractive() && statusBarStateController.state != StatusBarState.SHADE) {
+            shadeController.animateCollapseShadeForced()
+            return true
+        }
+        return false
+    }
+
+    private fun dispatchVolumeKeyEvent(event: KeyEvent): Boolean {
+        mediaSessionLegacyHelperWrapper
+            .getHelper(context)
+            .sendVolumeKeyEvent(event, AudioManager.USE_DEFAULT_STREAM_TYPE, true)
+        return true
+    }
+
+    private fun isDeviceInteractive(): Boolean {
+        return keyguardInteractor.wakefulnessModel.value.isDeviceInteractive()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt
new file mode 100644
index 0000000..9924369
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/util/MediaSessionLegacyHelperWrapper.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.media.controls.util
+
+import android.content.Context
+import android.media.session.MediaSessionLegacyHelper
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+
+/** Injectable wrapper around `MediaSessionLegacyHelper` functions */
+@SysUISingleton
+class MediaSessionLegacyHelperWrapper @Inject constructor() {
+    fun getHelper(context: Context): MediaSessionLegacyHelper {
+        return MediaSessionLegacyHelper.getHelper(context)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index e134f7c..ae0ab84 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -25,6 +25,7 @@
 import static android.app.StatusBarManager.WindowVisibleState;
 import static android.app.StatusBarManager.windowStateToString;
 import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
+import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR;
 import static android.view.InsetsSource.FLAG_SUPPRESS_SCRIM;
 import static android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
@@ -1714,10 +1715,12 @@
 
     private InsetsFrameProvider[] getInsetsFrameProvider(int insetsHeight, Context userContext) {
         final InsetsFrameProvider navBarProvider =
-                new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars())
-                        .setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] {
-                                new InsetsFrameProvider.InsetsSizeOverride(
-                                        TYPE_INPUT_METHOD, null)});
+                new InsetsFrameProvider(mInsetsSourceOwner, 0, WindowInsets.Type.navigationBars());
+        if (!ENABLE_HIDE_IME_CAPTION_BAR) {
+            navBarProvider.setInsetsSizeOverrides(new InsetsFrameProvider.InsetsSizeOverride[] {
+                    new InsetsFrameProvider.InsetsSizeOverride(TYPE_INPUT_METHOD, null)
+            });
+        }
         if (insetsHeight != -1 && !mEdgeBackGestureHandler.isButtonForcedVisible()) {
             navBarProvider.setInsetsSize(Insets.of(0, 0, 0, insetsHeight));
         }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 132cd61..df7d88f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -163,6 +163,7 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController.StateListener;
+import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.transition.ShadeTransitionController;
 import com.android.systemui.shared.system.InteractionJankMonitorWrapper;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -355,6 +356,7 @@
     private final AlternateBouncerInteractor mAlternateBouncerInteractor;
     private final KeyguardRootView mKeyguardRootView;
     private final QuickSettingsController mQsController;
+    private final ShadeInteractor mShadeInteractor;
     private final TouchHandler mTouchHandler = new TouchHandler();
 
     private long mDownTime;
@@ -559,7 +561,6 @@
     private float mHintDistance;
     private float mInitialOffsetOnTouch;
     private boolean mCollapsedAndHeadsUpOnDown;
-    private float mExpandedFraction = 0;
     private float mExpansionDragDownAmountPx = 0;
     private boolean mPanelClosedOnDown;
     private boolean mHasLayoutedSinceDown;
@@ -709,10 +710,12 @@
             VibratorHelper vibratorHelper,
             LatencyTracker latencyTracker,
             PowerManager powerManager,
-            AccessibilityManager accessibilityManager, @DisplayId int displayId,
+            AccessibilityManager accessibilityManager,
+            @DisplayId int displayId,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
             MetricsLogger metricsLogger,
             ShadeLogger shadeLogger,
+            ShadeInteractor shadeInteractor,
             ConfigurationController configurationController,
             Provider<FlingAnimationUtils.Builder> flingAnimationUtilsBuilder,
             StatusBarTouchableRegionManager statusBarTouchableRegionManager,
@@ -785,6 +788,7 @@
         mView = view;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mLockscreenGestureLogger = lockscreenGestureLogger;
+        mShadeInteractor = shadeInteractor;
         mShadeExpansionStateManager = shadeExpansionStateManager;
         mShadeLog = shadeLogger;
         mGutsManager = gutsManager;
@@ -2244,7 +2248,7 @@
             if (!isFalseTouch(x, y, interactionType)) {
                 mShadeLog.logFlingExpands(vel, vectorVel, interactionType,
                         this.mFlingAnimationUtils.getMinVelocityPxPerSecond(),
-                        mExpandedFraction > 0.5f, mAllowExpandForSmallExpansion);
+                        getExpandedFraction() > 0.5f, mAllowExpandForSmallExpansion);
                 if (Math.abs(vectorVel) < this.mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
                     expands = shouldExpandWhenNotFlinging();
                 } else {
@@ -2419,7 +2423,7 @@
     void requestScrollerTopPaddingUpdate(boolean animate) {
         mNotificationStackScrollLayoutController.updateTopPadding(
                 mQsController.calculateNotificationsTopPadding(mIsExpandingOrCollapsing,
-                        getKeyguardNotificationStaticPadding(), mExpandedFraction), animate);
+                        getKeyguardNotificationStaticPadding(), getExpandedFraction()), animate);
         if (isKeyguardShowing()
                 && mKeyguardBypassController.getBypassEnabled()) {
             // update the position of the header
@@ -2501,10 +2505,10 @@
     private void onHeightUpdated(float expandedHeight) {
         if (expandedHeight <= 0) {
             mShadeLog.logExpansionChanged("onHeightUpdated: fully collapsed.",
-                    mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+                    getExpandedFraction(), isExpanded(), mTracking, mExpansionDragDownAmountPx);
         } else if (isFullyExpanded()) {
             mShadeLog.logExpansionChanged("onHeightUpdated: fully expanded.",
-                    mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+                    getExpandedFraction(), isExpanded(), mTracking, mExpansionDragDownAmountPx);
         }
         if (!mQsController.getExpanded() || mQsController.isExpandImmediate()
                 || mIsExpandingOrCollapsing && mQsController.getExpandedWhenExpandingStarted()) {
@@ -3198,6 +3202,11 @@
         }
     }
 
+    @Override
+    public void performHapticFeedback(int constant) {
+        mVibratorHelper.performHapticFeedback(mView, constant);
+    }
+
     private class ShadeHeadsUpTrackerImpl implements ShadeHeadsUpTracker {
         @Override
         public void addTrackingHeadsUpListener(Consumer<ExpandableNotificationRow> listener) {
@@ -3425,7 +3434,7 @@
         ipw.print("mHintDistance="); ipw.println(mHintDistance);
         ipw.print("mInitialOffsetOnTouch="); ipw.println(mInitialOffsetOnTouch);
         ipw.print("mCollapsedAndHeadsUpOnDown="); ipw.println(mCollapsedAndHeadsUpOnDown);
-        ipw.print("mExpandedFraction="); ipw.println(mExpandedFraction);
+        ipw.print("getExpandedFraction()="); ipw.println(getExpandedFraction());
         ipw.print("mExpansionDragDownAmountPx="); ipw.println(mExpansionDragDownAmountPx);
         ipw.print("mPanelClosedOnDown="); ipw.println(mPanelClosedOnDown);
         ipw.print("mHasLayoutedSinceDown="); ipw.println(mHasLayoutedSinceDown);
@@ -3761,7 +3770,7 @@
 
             // don't fling while in keyguard to avoid jump in shade expand animation;
             // touch has been intercepted already so flinging here is redundant
-            if (mBarState == KEYGUARD && mExpandedFraction >= 1.0) {
+            if (mBarState == KEYGUARD && getExpandedFraction() >= 1.0) {
                 mShadeLog.d("NPVC endMotionEvent - skipping fling on keyguard");
             } else {
                 fling(vel, expand, isFalseTouch(x, y, interactionType));
@@ -3857,6 +3866,16 @@
     @VisibleForTesting
     void setExpandedHeight(float height) {
         debugLog("setExpandedHeight(%.1f)", height);
+        int maxPanelTransitionDistance = getMaxPanelTransitionDistance();
+        if (maxPanelTransitionDistance == 0) {
+            setExpandedFracAndHeight(0, height);
+        } else {
+            setExpandedFracAndHeight(height / maxPanelTransitionDistance, height);
+        }
+    }
+
+    private void setExpandedFracAndHeight(float frac, float height) {
+        mShadeInteractor.setExpansion(frac);
         setExpandedHeightInternal(height);
     }
 
@@ -3912,11 +3931,9 @@
                     mHeightAnimator.end();
                 }
             }
-            mExpandedFraction = Math.min(1f,
-                    maxPanelHeight == 0 ? 0 : mExpandedHeight / maxPanelHeight);
-            mQsController.setShadeExpansion(mExpandedHeight, mExpandedFraction);
+            mQsController.setShadeExpansion(mExpandedHeight, getExpandedFraction());
             mExpansionDragDownAmountPx = h;
-            mAmbientState.setExpansionFraction(mExpandedFraction);
+            mAmbientState.setExpansionFraction(getExpandedFraction());
             onHeightUpdated(mExpandedHeight);
             updateExpansionAndVisibility();
         });
@@ -3944,8 +3961,7 @@
     /** Sets the expanded height relative to a number from 0 to 1. */
     @VisibleForTesting
     void setExpandedFraction(float frac) {
-        final int maxDist = getMaxPanelTransitionDistance();
-        setExpandedHeight(maxDist * frac);
+        setExpandedFracAndHeight(frac, getMaxPanelTransitionDistance() * frac);
     }
 
     float getExpandedHeight() {
@@ -3953,7 +3969,7 @@
     }
 
     float getExpandedFraction() {
-        return mExpandedFraction;
+        return mShadeInteractor.getExpansion().getValue();
     }
 
     @Override
@@ -3975,7 +3991,7 @@
 
     @Override
     public boolean isFullyCollapsed() {
-        return mExpandedFraction <= 0.0f;
+        return getExpandedFraction() <= 0.0f;
     }
 
     @Override
@@ -4134,7 +4150,7 @@
                                         animator.getAnimatedFraction()));
                         setOverExpansionInternal(expansion, false /* isFromGesture */);
                     }
-                    setExpandedHeightInternal((float) animation.getAnimatedValue());
+                    setExpandedHeight((float) animation.getAnimatedValue());
                 });
         return animator;
     }
@@ -4148,14 +4164,14 @@
     @Override
     public void updateExpansionAndVisibility() {
         mShadeExpansionStateManager.onPanelExpansionChanged(
-                mExpandedFraction, isExpanded(), mTracking, mExpansionDragDownAmountPx);
+                getExpandedFraction(), isExpanded(), mTracking, mExpansionDragDownAmountPx);
 
         updateVisibility();
     }
 
     @Override
     public boolean isExpanded() {
-        return mExpandedFraction > 0f
+        return getExpandedFraction() > 0f
                 || mInstantExpanding
                 || isPanelVisibleBecauseOfHeadsUp()
                 || mTracking
@@ -4913,7 +4929,7 @@
                     mMotionAborted = false;
                     mPanelClosedOnDown = isFullyCollapsed();
                     mShadeLog.logPanelClosedOnDown("intercept down touch", mPanelClosedOnDown,
-                            mExpandedFraction);
+                            getExpandedFraction());
                     mCollapsedAndHeadsUpOnDown = false;
                     mHasLayoutedSinceDown = false;
                     mUpdateFlingOnLayout = false;
@@ -5132,7 +5148,7 @@
                     mMinExpandHeight = 0.0f;
                     mPanelClosedOnDown = isFullyCollapsed();
                     mShadeLog.logPanelClosedOnDown("handle down touch", mPanelClosedOnDown,
-                            mExpandedFraction);
+                            getExpandedFraction());
                     mHasLayoutedSinceDown = false;
                     mUpdateFlingOnLayout = false;
                     mMotionAborted = false;
@@ -5191,7 +5207,7 @@
                     if (isFullyCollapsed()) {
                         // If panel is fully collapsed, reset haptic effect before adding movement.
                         mHasVibratedOnOpen = false;
-                        mShadeLog.logHasVibrated(mHasVibratedOnOpen, mExpandedFraction);
+                        mShadeLog.logHasVibrated(mHasVibratedOnOpen, getExpandedFraction());
                     }
                     addMovement(event);
                     if (!isFullyCollapsed()) {
@@ -5227,7 +5243,7 @@
                         // otherwise {@link NotificationStackScrollLayout}
                         // wrongly enables stack height updates at the start of lockscreen swipe-up
                         mAmbientState.setSwipingUp(h <= 0);
-                        setExpandedHeightInternal(newHeight);
+                        setExpandedHeight(newHeight);
                     }
                     break;
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
index bea12de..6564118 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeHeaderController.kt
@@ -263,9 +263,11 @@
                     resources.getDimensionPixelSize(
                         R.dimen.shade_header_system_icons_padding_start
                     ),
-                    systemIcons.paddingTop,
+                    resources.getDimensionPixelSize(R.dimen.shade_header_system_icons_padding_top),
                     resources.getDimensionPixelSize(R.dimen.shade_header_system_icons_padding_end),
-                    systemIcons.paddingBottom
+                    resources.getDimensionPixelSize(
+                        R.dimen.shade_header_system_icons_padding_bottom
+                    )
                 )
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
index d5b5c87..182a676 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
@@ -248,6 +248,16 @@
     /** Starts tracking a shade expansion gesture that originated from the status bar. */
     fun startTrackingExpansionFromStatusBar()
 
+    /**
+     * Performs haptic feedback from a view with a haptic feedback constant.
+     *
+     * The implementation of this method should use the [android.view.View.performHapticFeedback]
+     * method with the provided constant.
+     *
+     * @param[constant] One of [android.view.HapticFeedbackConstants]
+     */
+    fun performHapticFeedback(constant: Int)
+
     // ******* End Keyguard Section *********
 
     /** Returns the ShadeHeadsUpTracker. */
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
index 287ac52..09b74b2 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
@@ -86,6 +86,8 @@
         return false
     }
     override fun startTrackingExpansionFromStatusBar() {}
+    override fun performHapticFeedback(constant: Int) {}
+
     override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl()
     override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
index ebb9935..76dca4c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeRepository.kt
@@ -34,13 +34,34 @@
     /** ShadeModel information regarding shade expansion events */
     val shadeModel: Flow<ShadeModel>
 
-    /** Amount qs has expanded. Quick Settings can be expanded without the full shade expansion. */
+    /**
+     * Value from `0` to `1` representing the amount the shade has expanded where `1` is fully
+     * expanded and `0` is fully collapsed. Quick Settings can be expanded without a fully expanded
+     * shade.
+     */
     val qsExpansion: StateFlow<Float>
 
+    /**
+     * Value from `0` to `1` representing the amount the shade has expanded where `1` is fully
+     * expanded and `0` is fully collapsed.
+     */
+    val expansion: StateFlow<Float>
+
     /** Amount shade has expanded with regard to the UDFPS location */
     val udfpsTransitionToFullShadeProgress: StateFlow<Float>
 
+    /**
+     * Set shade expansion to a value from `0` to `1` representing the amount the shade has expanded
+     * where `1` is fully expanded and `0` is fully collapsed.
+     */
+    fun setExpansion(expansion: Float)
+
+    /**
+     * Set quick settings expansion to a value from `0` to `1` representing the amount quick
+     * settings has expanded where `1` is fully expanded and `0` is fully collapsed.
+     */
     fun setQsExpansion(qsExpansion: Float)
+
     fun setUdfpsTransitionToFullShadeProgress(progress: Float)
 }
 
@@ -78,9 +99,17 @@
     private val _qsExpansion = MutableStateFlow(0f)
     override val qsExpansion: StateFlow<Float> = _qsExpansion.asStateFlow()
 
+    private val _expansion = MutableStateFlow(0f)
+    override val expansion: StateFlow<Float> = _expansion.asStateFlow()
+
     private var _udfpsTransitionToFullShadeProgress = MutableStateFlow(0f)
     override val udfpsTransitionToFullShadeProgress: StateFlow<Float> =
         _udfpsTransitionToFullShadeProgress.asStateFlow()
+
+    override fun setExpansion(expansion: Float) {
+        _expansion.value = expansion
+    }
+
     override fun setQsExpansion(qsExpansion: Float) {
         _qsExpansion.value = qsExpansion
     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
index 6fde84a..efe3eb4 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt
@@ -19,6 +19,7 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.statusbar.disableflags.data.repository.DisableFlagsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
@@ -38,18 +39,29 @@
 @Inject
 constructor(
     @Application scope: CoroutineScope,
+    private val shadeRepository: ShadeRepository,
     disableFlagsRepository: DisableFlagsRepository,
     keyguardRepository: KeyguardRepository,
     userSetupRepository: UserSetupRepository,
     deviceProvisionedController: DeviceProvisionedController,
     userInteractor: UserInteractor,
 ) {
+    /**
+     * Value from `0` to `1` representing the amount the shade has expanded where `1` is fully
+     * expanded and `0` is fully collapsed.
+     */
+    val expansion = shadeRepository.expansion
+
     /** Emits true if the shade is currently allowed and false otherwise. */
     val isShadeEnabled: StateFlow<Boolean> =
         disableFlagsRepository.disableFlags
             .map { it.isShadeEnabled() }
             .stateIn(scope, SharingStarted.Eagerly, initialValue = false)
 
+    fun setExpansion(expansion: Float) {
+        shadeRepository.setExpansion(expansion)
+    }
+
     /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */
     val isExpandToQsEnabled: Flow<Boolean> =
         combine(
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 6431ef9..2bc7b99 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_AWAKE;
 import static com.android.systemui.keyguard.WakefulnessLifecycle.WAKEFULNESS_WAKING;
 
@@ -33,12 +34,15 @@
 import android.os.Vibrator;
 import android.util.Log;
 import android.util.Slog;
+import android.view.HapticFeedbackConstants;
 import android.view.KeyEvent;
 import android.view.WindowInsets;
 import android.view.WindowInsets.Type.InsetsType;
 import android.view.WindowInsetsController.Appearance;
 import android.view.WindowInsetsController.Behavior;
 
+import androidx.annotation.VisibleForTesting;
+
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.statusbar.LetterboxDetails;
@@ -49,6 +53,7 @@
 import com.android.systemui.camera.CameraIntents;
 import com.android.systemui.dagger.qualifiers.DisplayId;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.qs.QSHost;
@@ -107,6 +112,7 @@
     private final Lazy<CameraLauncher> mCameraLauncherLazy;
     private final QuickSettingsController mQsController;
     private final QSHost mQSHost;
+    private final FeatureFlags mFeatureFlags;
 
     private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
@@ -144,7 +150,8 @@
             Lazy<CameraLauncher> cameraLauncherLazy,
             UserTracker userTracker,
             QSHost qsHost,
-            ActivityStarter activityStarter) {
+            ActivityStarter activityStarter,
+            FeatureFlags featureFlags) {
         mCentralSurfaces = centralSurfaces;
         mQsController = quickSettingsController;
         mContext = context;
@@ -171,6 +178,7 @@
         mCameraLauncherLazy = cameraLauncherLazy;
         mUserTracker = userTracker;
         mQSHost = qsHost;
+        mFeatureFlags = featureFlags;
 
         mVibrateOnOpening = resources.getBoolean(R.bool.config_vibrateOnIconAnimation);
         mCameraLaunchGestureVibrationEffect = getCameraGestureVibrationEffect(
@@ -314,7 +322,7 @@
             mMetricsLogger.action(MetricsEvent.ACTION_SYSTEM_NAVIGATION_KEY_DOWN);
             if (mShadeViewController.isFullyCollapsed()) {
                 if (mVibrateOnOpening) {
-                    mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+                    vibrateOnNavigationKeyDown();
                 }
                 mShadeViewController.expand(true /* animate */);
                 mNotificationStackScrollLayoutController.setWillExpand(true);
@@ -587,4 +595,15 @@
         }
         return VibrationEffect.createWaveform(timings, /* repeat= */ -1);
     }
+
+    @VisibleForTesting
+    void vibrateOnNavigationKeyDown() {
+        if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) {
+            mShadeViewController.performHapticFeedback(
+                    HapticFeedbackConstants.GESTURE_START
+            );
+        } else {
+            mVibratorHelper.vibrate(VibrationEffect.EFFECT_TICK);
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt
index 4a31b86..eaae0f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/VariableDateViewController.kt
@@ -33,6 +33,7 @@
 import com.android.systemui.Dependency
 import com.android.systemui.broadcast.BroadcastDispatcher
 import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.shade.ShadeLogger
 import com.android.systemui.util.ViewController
 import com.android.systemui.util.time.SystemClock
 import java.text.FieldPosition
@@ -83,6 +84,7 @@
     private val systemClock: SystemClock,
     private val broadcastDispatcher: BroadcastDispatcher,
     private val shadeExpansionStateManager: ShadeExpansionStateManager,
+    private val shadeLogger: ShadeLogger,
     private val timeTickHandler: Handler,
     view: VariableDateView
 ) : ViewController<VariableDateView>(view) {
@@ -111,24 +113,29 @@
 
     private val intentReceiver: BroadcastReceiver = object : BroadcastReceiver() {
         override fun onReceive(context: Context, intent: Intent) {
+            val action = intent.action
+            if (
+                    Intent.ACTION_LOCALE_CHANGED == action ||
+                    Intent.ACTION_TIMEZONE_CHANGED == action
+            ) {
+                // need to get a fresh date format
+                dateFormat = null
+                shadeLogger.d("VariableDateViewController received intent to refresh date format")
+            }
+
+            val handler = mView.handler
+
             // If the handler is null, it means we received a broadcast while the view has not
             // finished being attached or in the process of being detached.
             // In that case, do not post anything.
-            val handler = mView.handler ?: return
-            val action = intent.action
-            if (
+            if (handler == null) {
+                shadeLogger.d("VariableDateViewController received intent but handler was null")
+            } else if (
                     Intent.ACTION_TIME_TICK == action ||
                     Intent.ACTION_TIME_CHANGED == action ||
                     Intent.ACTION_TIMEZONE_CHANGED == action ||
                     Intent.ACTION_LOCALE_CHANGED == action
             ) {
-                if (
-                        Intent.ACTION_LOCALE_CHANGED == action ||
-                        Intent.ACTION_TIMEZONE_CHANGED == action
-                ) {
-                    // need to get a fresh date format
-                    handler.post { dateFormat = null }
-                }
                 handler.post(::updateClock)
             }
         }
@@ -231,6 +238,7 @@
         private val systemClock: SystemClock,
         private val broadcastDispatcher: BroadcastDispatcher,
         private val shadeExpansionStateManager: ShadeExpansionStateManager,
+        private val shadeLogger: ShadeLogger,
         @Named(Dependency.TIME_TICK_HANDLER_NAME) private val handler: Handler
     ) {
         fun create(view: VariableDateView): VariableDateViewController {
@@ -238,6 +246,7 @@
                     systemClock,
                     broadcastDispatcher,
                     shadeExpansionStateManager,
+                    shadeLogger,
                     handler,
                     view
             )
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
index b349696..438f0f4 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardDisplayManagerTest.java
@@ -18,6 +18,8 @@
 
 import static android.view.DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS;
 
+import static com.android.systemui.flags.Flags.ENABLE_CLOCK_KEYGUARD_PRESENTATION;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
@@ -37,6 +39,7 @@
 
 import com.android.keyguard.dagger.KeyguardStatusViewComponent;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.navigationbar.NavigationBarController;
 import com.android.systemui.settings.FakeDisplayTracker;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
@@ -59,8 +62,13 @@
     @Mock
     private KeyguardStatusViewComponent.Factory mKeyguardStatusViewComponentFactory;
     @Mock
+    private ConnectedDisplayKeyguardPresentation.Factory
+            mConnectedDisplayKeyguardPresentationFactory;
+    @Mock
     private KeyguardDisplayManager.KeyguardPresentation mKeyguardPresentation;
     @Mock
+    private ConnectedDisplayKeyguardPresentation mConnectedDisplayKeyguardPresentation;
+    @Mock
     private KeyguardDisplayManager.DeviceStateHelper mDeviceStateHelper;
     @Mock
     private KeyguardStateController mKeyguardStateController;
@@ -69,7 +77,7 @@
     private Executor mBackgroundExecutor = Runnable::run;
     private KeyguardDisplayManager mManager;
     private FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext);
-
+    private FakeFeatureFlags mFakeFeatureFlags = new FakeFeatureFlags();
     // The default and secondary displays are both in the default group
     private Display mDefaultDisplay;
     private Display mSecondaryDisplay;
@@ -80,10 +88,14 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
+        mFakeFeatureFlags.set(ENABLE_CLOCK_KEYGUARD_PRESENTATION, false);
         mManager = spy(new KeyguardDisplayManager(mContext, () -> mNavigationBarController,
                 mKeyguardStatusViewComponentFactory, mDisplayTracker, mMainExecutor,
-                mBackgroundExecutor, mDeviceStateHelper, mKeyguardStateController));
+                mBackgroundExecutor, mDeviceStateHelper, mKeyguardStateController,
+                mConnectedDisplayKeyguardPresentationFactory, mFakeFeatureFlags));
         doReturn(mKeyguardPresentation).when(mManager).createPresentation(any());
+        doReturn(mConnectedDisplayKeyguardPresentation).when(
+                mConnectedDisplayKeyguardPresentationFactory).create(any());
 
         mDefaultDisplay = new Display(DisplayManagerGlobal.getInstance(), Display.DEFAULT_DISPLAY,
                 new DisplayInfo(), DEFAULT_DISPLAY_ADJUSTMENTS);
@@ -138,4 +150,15 @@
         when(mKeyguardStateController.isOccluded()).thenReturn(true);
         verify(mManager, never()).createPresentation(eq(mSecondaryDisplay));
     }
+
+    @Test
+    public void testShow_withClockPresentationFlagEnabled_presentationCreated() {
+        when(mManager.createPresentation(any())).thenCallRealMethod();
+        mFakeFeatureFlags.set(ENABLE_CLOCK_KEYGUARD_PRESENTATION, true);
+        mDisplayTracker.setAllDisplays(new Display[]{mDefaultDisplay, mSecondaryDisplay});
+
+        mManager.show();
+
+        verify(mConnectedDisplayKeyguardPresentationFactory).create(eq(mSecondaryDisplay));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java
index 362d26b0..cf4e2c319 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/BiometricNotificationDialogFactoryTest.java
@@ -18,6 +18,7 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.junit.Assume.assumeTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
@@ -28,6 +29,7 @@
 import android.content.Context;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.content.pm.PackageManager;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
@@ -86,6 +88,9 @@
 
     @Test
     public void testFingerprintReEnrollDialog_onRemovalSucceeded() {
+        assumeTrue(getContext().getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT));
+
         mDialogFactory.createReenrollDialog(mContextSpy, mDialog,
                 BiometricSourceType.FINGERPRINT);
 
@@ -109,6 +114,9 @@
 
     @Test
     public void testFingerprintReEnrollDialog_onRemovalError() {
+        assumeTrue(getContext().getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_FINGERPRINT));
+
         mDialogFactory.createReenrollDialog(mContextSpy, mDialog,
                 BiometricSourceType.FINGERPRINT);
 
@@ -130,6 +138,9 @@
 
     @Test
     public void testFaceReEnrollDialog_onRemovalSucceeded() {
+        assumeTrue(getContext().getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_FACE));
+
         mDialogFactory.createReenrollDialog(mContextSpy, mDialog,
                 BiometricSourceType.FACE);
 
@@ -153,6 +164,9 @@
 
     @Test
     public void testFaceReEnrollDialog_onRemovalError() {
+        assumeTrue(getContext().getPackageManager()
+                .hasSystemFeature(PackageManager.FEATURE_FACE));
+
         mDialogFactory.createReenrollDialog(mContextSpy, mDialog,
                 BiometricSourceType.FACE);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
new file mode 100644
index 0000000..632d149
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyevent/domain/interactor/KeyEventInteractorTest.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.keyevent.domain.interactor
+
+import android.view.KeyEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.back.domain.interactor.BackActionInteractor
+import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
+import com.android.systemui.keyguard.domain.interactor.KeyguardKeyEventInteractor
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyEventInteractorTest : SysuiTestCase() {
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    private lateinit var keyguardInteractorWithDependencies:
+        KeyguardInteractorFactory.WithDependencies
+    @Mock private lateinit var keyguardKeyEventInteractor: KeyguardKeyEventInteractor
+    @Mock private lateinit var backActionInteractor: BackActionInteractor
+
+    private lateinit var underTest: KeyEventInteractor
+
+    @Before
+    fun setup() {
+        keyguardInteractorWithDependencies = KeyguardInteractorFactory.create()
+        underTest =
+            KeyEventInteractor(
+                backActionInteractor,
+                keyguardKeyEventInteractor,
+            )
+    }
+
+    @Test
+    fun dispatchBackKey_notHandledByKeyguardKeyEventInteractor_handledByBackActionInteractor() {
+        val backKeyEventActionDown = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK)
+        val backKeyEventActionUp = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)
+
+        // GIVEN back key ACTION_DOWN and ACTION_UP aren't handled by the keyguardKeyEventInteractor
+        whenever(keyguardKeyEventInteractor.dispatchKeyEvent(backKeyEventActionDown))
+            .thenReturn(false)
+        whenever(keyguardKeyEventInteractor.dispatchKeyEvent(backKeyEventActionUp))
+            .thenReturn(false)
+
+        // WHEN back key event ACTION_DOWN, the event is handled even though back isn't requested
+        assertThat(underTest.dispatchKeyEvent(backKeyEventActionDown)).isTrue()
+        // THEN back event isn't handled on ACTION_DOWN
+        verify(backActionInteractor, never()).onBackRequested()
+
+        // WHEN back key event ACTION_UP
+        assertThat(underTest.dispatchKeyEvent(backKeyEventActionUp)).isTrue()
+        // THEN back event is handled on ACTION_UP
+        verify(backActionInteractor).onBackRequested()
+    }
+
+    @Test
+    fun dispatchKeyEvent_isNotHandledByKeyguardKeyEventInteractor() {
+        val keyEvent =
+            KeyEvent(
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        whenever(keyguardKeyEventInteractor.dispatchKeyEvent(eq(keyEvent))).thenReturn(false)
+        assertThat(underTest.dispatchKeyEvent(keyEvent)).isFalse()
+    }
+
+    @Test
+    fun dispatchKeyEvent_handledByKeyguardKeyEventInteractor() {
+        val keyEvent =
+            KeyEvent(
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        whenever(keyguardKeyEventInteractor.dispatchKeyEvent(eq(keyEvent))).thenReturn(true)
+        assertThat(underTest.dispatchKeyEvent(keyEvent)).isTrue()
+    }
+
+    @Test
+    fun interceptMediaKey_isNotHandledByKeyguardKeyEventInteractor() {
+        val keyEvent =
+            KeyEvent(
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        whenever(keyguardKeyEventInteractor.interceptMediaKey(eq(keyEvent))).thenReturn(false)
+        assertThat(underTest.interceptMediaKey(keyEvent)).isFalse()
+    }
+
+    @Test
+    fun interceptMediaKey_handledByKeyguardKeyEventInteractor() {
+        val keyEvent =
+            KeyEvent(
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        whenever(keyguardKeyEventInteractor.interceptMediaKey(eq(keyEvent))).thenReturn(true)
+        assertThat(underTest.interceptMediaKey(keyEvent)).isTrue()
+    }
+
+    @Test
+    fun dispatchKeyEventPreIme_isNotHandledByKeyguardKeyEventInteractor() {
+        val keyEvent =
+            KeyEvent(
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        whenever(keyguardKeyEventInteractor.dispatchKeyEventPreIme(eq(keyEvent))).thenReturn(false)
+        assertThat(underTest.dispatchKeyEventPreIme(keyEvent)).isFalse()
+    }
+
+    @Test
+    fun dispatchKeyEventPreIme_handledByKeyguardKeyEventInteractor() {
+        val keyEvent =
+            KeyEvent(
+                KeyEvent.ACTION_UP,
+                KeyEvent.KEYCODE_SPACE,
+            )
+        whenever(keyguardKeyEventInteractor.dispatchKeyEventPreIme(eq(keyEvent))).thenReturn(true)
+        assertThat(underTest.dispatchKeyEventPreIme(keyEvent)).isTrue()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
new file mode 100644
index 0000000..a3f7fc5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardKeyEventInteractorTest.kt
@@ -0,0 +1,255 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import android.media.AudioManager
+import android.media.session.MediaSessionLegacyHelper
+import android.view.KeyEvent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.back.domain.interactor.BackActionInteractor
+import com.android.systemui.keyguard.shared.model.WakeSleepReason
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
+import com.android.systemui.keyguard.shared.model.WakefulnessState
+import com.android.systemui.media.controls.util.MediaSessionLegacyHelperWrapper
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.clearInvocations
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class KeyguardKeyEventInteractorTest : SysuiTestCase() {
+    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
+
+    private val actionDownVolumeDownKeyEvent =
+        KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_DOWN)
+    private val actionDownVolumeUpKeyEvent =
+        KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_VOLUME_UP)
+    private val backKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK)
+    private val awakeWakefulnessMode =
+        WakefulnessModel(WakefulnessState.AWAKE, WakeSleepReason.OTHER, WakeSleepReason.OTHER)
+    private val asleepWakefulnessMode =
+        WakefulnessModel(WakefulnessState.ASLEEP, WakeSleepReason.OTHER, WakeSleepReason.OTHER)
+
+    private lateinit var keyguardInteractorWithDependencies:
+        KeyguardInteractorFactory.WithDependencies
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
+    @Mock private lateinit var shadeController: ShadeController
+    @Mock private lateinit var mediaSessionLegacyHelperWrapper: MediaSessionLegacyHelperWrapper
+    @Mock private lateinit var mediaSessionLegacyHelper: MediaSessionLegacyHelper
+    @Mock private lateinit var backActionInteractor: BackActionInteractor
+
+    private lateinit var underTest: KeyguardKeyEventInteractor
+
+    @Before
+    fun setup() {
+        whenever(mediaSessionLegacyHelperWrapper.getHelper(any()))
+            .thenReturn(mediaSessionLegacyHelper)
+        keyguardInteractorWithDependencies = KeyguardInteractorFactory.create()
+        underTest =
+            KeyguardKeyEventInteractor(
+                context,
+                statusBarStateController,
+                keyguardInteractorWithDependencies.keyguardInteractor,
+                statusBarKeyguardViewManager,
+                shadeController,
+                mediaSessionLegacyHelperWrapper,
+                backActionInteractor,
+            )
+    }
+
+    @Test
+    fun dispatchKeyEvent_volumeKey_dozing_handlesEvents() {
+        whenever(statusBarStateController.isDozing).thenReturn(true)
+
+        assertThat(underTest.dispatchKeyEvent(actionDownVolumeDownKeyEvent)).isTrue()
+        verify(mediaSessionLegacyHelper)
+            .sendVolumeKeyEvent(
+                eq(actionDownVolumeDownKeyEvent),
+                eq(AudioManager.USE_DEFAULT_STREAM_TYPE),
+                eq(true)
+            )
+
+        assertThat(underTest.dispatchKeyEvent(actionDownVolumeUpKeyEvent)).isTrue()
+        verify(mediaSessionLegacyHelper)
+            .sendVolumeKeyEvent(
+                eq(actionDownVolumeUpKeyEvent),
+                eq(AudioManager.USE_DEFAULT_STREAM_TYPE),
+                eq(true)
+            )
+    }
+
+    @Test
+    fun dispatchKeyEvent_volumeKey_notDozing_doesNotHandleEvents() {
+        whenever(statusBarStateController.isDozing).thenReturn(false)
+
+        assertThat(underTest.dispatchKeyEvent(actionDownVolumeDownKeyEvent)).isFalse()
+        verify(mediaSessionLegacyHelper, never())
+            .sendVolumeKeyEvent(
+                eq(actionDownVolumeDownKeyEvent),
+                eq(AudioManager.USE_DEFAULT_STREAM_TYPE),
+                eq(true)
+            )
+
+        assertThat(underTest.dispatchKeyEvent(actionDownVolumeUpKeyEvent)).isFalse()
+        verify(mediaSessionLegacyHelper, never())
+            .sendVolumeKeyEvent(
+                eq(actionDownVolumeUpKeyEvent),
+                eq(AudioManager.USE_DEFAULT_STREAM_TYPE),
+                eq(true)
+            )
+    }
+
+    @Test
+    fun dispatchKeyEvent_menuActionUp_interactiveKeyguard_collapsesShade() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
+
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(shadeController).animateCollapseShadeForced()
+    }
+
+    @Test
+    fun dispatchKeyEvent_menuActionUp_interactiveShadeLocked_collapsesShade() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE_LOCKED)
+        whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
+
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MENU)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(shadeController).animateCollapseShadeForced()
+    }
+
+    @Test
+    fun dispatchKeyEvent_menuActionUp_nonInteractiveKeyguard_neverCollapsesShade() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(asleepWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(statusBarKeyguardViewManager.shouldDismissOnMenuPressed()).thenReturn(true)
+
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MENU)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+    }
+
+    @Test
+    fun dispatchKeyEvent_spaceActionUp_interactiveKeyguard_collapsesShade() {
+        keyguardInteractorWithDependencies.repository.setWakefulnessModel(awakeWakefulnessMode)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+
+        // action down: does NOT collapse the shade
+        val actionDownMenuKeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_SPACE)
+        assertThat(underTest.dispatchKeyEvent(actionDownMenuKeyEvent)).isFalse()
+        verify(shadeController, never()).animateCollapseShadeForced()
+
+        // action up: collapses the shade
+        val actionUpMenuKeyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_SPACE)
+        assertThat(underTest.dispatchKeyEvent(actionUpMenuKeyEvent)).isTrue()
+        verify(shadeController).animateCollapseShadeForced()
+    }
+
+    @Test
+    fun dispatchKeyEventPreIme_back_keyguard_onBackRequested() {
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()).thenReturn(true)
+
+        whenever(backActionInteractor.onBackRequested()).thenReturn(false)
+        assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isFalse()
+        verify(backActionInteractor).onBackRequested()
+        clearInvocations(backActionInteractor)
+
+        whenever(backActionInteractor.onBackRequested()).thenReturn(true)
+        assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isTrue()
+        verify(backActionInteractor).onBackRequested()
+    }
+
+    @Test
+    fun dispatchKeyEventPreIme_back_keyguard_SBKVMdoesNotHandle_neverOnBackRequested() {
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()).thenReturn(false)
+        whenever(backActionInteractor.onBackRequested()).thenReturn(true)
+
+        assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isFalse()
+        verify(backActionInteractor, never()).onBackRequested()
+    }
+
+    @Test
+    fun dispatchKeyEventPreIme_back_shade_neverOnBackRequested() {
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+        whenever(statusBarKeyguardViewManager.dispatchBackKeyEventPreIme()).thenReturn(true)
+        whenever(backActionInteractor.onBackRequested()).thenReturn(true)
+
+        assertThat(underTest.dispatchKeyEventPreIme(backKeyEvent)).isFalse()
+        verify(backActionInteractor, never()).onBackRequested()
+    }
+
+    @Test
+    fun interceptMediaKey_keyguard_SBKVMdoesNotHandle_doesNotHandleMediaKey() {
+        val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(statusBarKeyguardViewManager.interceptMediaKey(eq(keyEvent))).thenReturn(false)
+
+        assertThat(underTest.interceptMediaKey(keyEvent)).isFalse()
+        verify(statusBarKeyguardViewManager).interceptMediaKey(eq(keyEvent))
+    }
+
+    @Test
+    fun interceptMediaKey_keyguard_handleMediaKey() {
+        val keyEvent = KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP)
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.KEYGUARD)
+        whenever(statusBarKeyguardViewManager.interceptMediaKey(eq(keyEvent))).thenReturn(true)
+
+        assertThat(underTest.interceptMediaKey(keyEvent)).isTrue()
+        verify(statusBarKeyguardViewManager).interceptMediaKey(eq(keyEvent))
+    }
+
+    @Test
+    fun interceptMediaKey_shade_doesNotHandleMediaKey() {
+        whenever(statusBarStateController.state).thenReturn(StatusBarState.SHADE)
+
+        assertThat(
+                underTest.interceptMediaKey(
+                    KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_VOLUME_UP)
+                )
+            )
+            .isFalse()
+        verify(statusBarKeyguardViewManager, never()).interceptMediaKey(any())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 981e44b..5cad9f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -73,6 +73,7 @@
 import com.android.keyguard.KeyguardStatusViewController;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.LockIconViewController;
+import com.android.keyguard.TestScopeProvider;
 import com.android.keyguard.dagger.KeyguardQsUserSwitchComponent;
 import com.android.keyguard.dagger.KeyguardStatusBarViewComponent;
 import com.android.keyguard.dagger.KeyguardStatusViewComponent;
@@ -119,6 +120,7 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.data.repository.ShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.transition.ShadeTransitionController;
@@ -134,6 +136,7 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.VibratorHelper;
+import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository;
 import com.android.systemui.statusbar.notification.ConversationNotificationManager;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
@@ -164,14 +167,17 @@
 import com.android.systemui.statusbar.phone.StatusBarTouchableRegionManager;
 import com.android.systemui.statusbar.phone.TapAgainViewController;
 import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.KeyguardQsUserSwitchController;
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView;
 import com.android.systemui.statusbar.window.StatusBarWindowStateController;
 import com.android.systemui.unfold.SysUIUnfoldComponent;
+import com.android.systemui.user.domain.interactor.UserInteractor;
 import com.android.systemui.util.kotlin.JavaAdapter;
 import com.android.systemui.util.time.FakeSystemClock;
 import com.android.systemui.util.time.SystemClock;
@@ -358,6 +364,16 @@
         mFakeKeyguardRepository = keyguardInteractorDeps.getRepository();
         mKeyguardBottomAreaInteractor = new KeyguardBottomAreaInteractor(mFakeKeyguardRepository);
         mKeyguardInteractor = keyguardInteractorDeps.getKeyguardInteractor();
+        mShadeRepository = new FakeShadeRepository();
+        mShadeInteractor = new ShadeInteractor(
+                TestScopeProvider.getTestScope(),
+                mShadeRepository,
+                new FakeDisableFlagsRepository(),
+                new FakeKeyguardRepository(),
+                new FakeUserSetupRepository(),
+                mock(DeviceProvisionedController.class),
+                mock(UserInteractor.class)
+        );
 
         SystemClock systemClock = new FakeSystemClock();
         mStatusBarStateController = new StatusBarStateControllerImpl(mUiEventLogger, mDumpManager,
@@ -584,19 +600,33 @@
                 mMainHandler,
                 mLayoutInflater,
                 mFeatureFlags,
-                coordinator, expansionHandler, mDynamicPrivacyController, mKeyguardBypassController,
-                mFalsingManager, new FalsingCollectorFake(),
+                coordinator,
+                expansionHandler,
+                mDynamicPrivacyController,
+                mKeyguardBypassController,
+                mFalsingManager,
+                new FalsingCollectorFake(),
                 mKeyguardStateController,
                 mStatusBarStateController,
                 mStatusBarWindowStateController,
                 mNotificationShadeWindowController,
-                mDozeLog, mDozeParameters, mCommandQueue, mVibratorHelper,
-                mLatencyTracker, mPowerManager, mAccessibilityManager, 0, mUpdateMonitor,
+                mDozeLog,
+                mDozeParameters,
+                mCommandQueue,
+                mVibratorHelper,
+                mLatencyTracker,
+                mPowerManager,
+                mAccessibilityManager,
+                0,
+                mUpdateMonitor,
                 mMetricsLogger,
                 mShadeLog,
+                mShadeInteractor,
                 mConfigurationController,
-                () -> flingAnimationUtilsBuilder, mStatusBarTouchableRegionManager,
-                mConversationNotificationManager, mMediaHierarchyManager,
+                () -> flingAnimationUtilsBuilder,
+                mStatusBarTouchableRegionManager,
+                mConversationNotificationManager,
+                mMediaHierarchyManager,
                 mStatusBarKeyguardViewManager,
                 mGutsManager,
                 mNotificationsQSContainerController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
index a2c2912..35a54d1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java
@@ -49,6 +49,7 @@
 import com.android.systemui.plugins.qs.QS;
 import com.android.systemui.qs.QSFragment;
 import com.android.systemui.screenrecord.RecordingController;
+import com.android.systemui.shade.data.repository.FakeShadeRepository;
 import com.android.systemui.shade.data.repository.ShadeRepository;
 import com.android.systemui.shade.domain.interactor.ShadeInteractor;
 import com.android.systemui.shade.transition.ShadeTransitionController;
@@ -170,6 +171,7 @@
         mShadeInteractor =
                 new ShadeInteractor(
                         mTestScope.getBackgroundScope(),
+                        new FakeShadeRepository(),
                         mDisableFlagsRepository,
                         mKeyguardRepository,
                         new FakeUserSetupRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
index cdcd1a2..9e6c12f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt
@@ -67,6 +67,7 @@
     private val featureFlags = FakeFeatureFlags()
     private val userSetupRepository = FakeUserSetupRepository()
     private val userRepository = FakeUserRepository()
+    private val shadeRepository = FakeShadeRepository()
     private val disableFlagsRepository = FakeDisableFlagsRepository()
     private val keyguardRepository = FakeKeyguardRepository()
 
@@ -138,6 +139,7 @@
         underTest =
             ShadeInteractor(
                 testScope.backgroundScope,
+                shadeRepository,
                 disableFlagsRepository,
                 keyguardRepository,
                 userSetupRepository,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
index 4a2518a..ec4367c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/LockscreenShadeTransitionControllerTest.kt
@@ -102,8 +102,10 @@
     @Mock lateinit var transitionControllerCallback: LockscreenShadeTransitionController.Callback
     private val disableFlagsRepository = FakeDisableFlagsRepository()
     private val keyguardRepository = FakeKeyguardRepository()
+    private val shadeRepository = FakeShadeRepository()
     private val shadeInteractor = ShadeInteractor(
         testScope.backgroundScope,
+        shadeRepository,
         disableFlagsRepository,
         keyguardRepository,
         userSetupRepository = FakeUserSetupRepository(),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
index 987861d..77c9b8b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt
@@ -1,6 +1,7 @@
 package com.android.systemui.statusbar.notification.stack
 
 import android.annotation.DimenRes
+import android.content.pm.PackageManager
 import android.widget.FrameLayout
 import androidx.test.filters.SmallTest
 import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
@@ -21,6 +22,7 @@
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import org.junit.Assume
 import org.junit.Before
 import org.junit.Rule
 import org.junit.Test
@@ -66,6 +68,8 @@
 
     @Before
     fun setUp() {
+        Assume.assumeFalse(isTv())
+
         whenever(notificationShelf.viewState).thenReturn(ExpandableViewState())
         whenever(notificationRow.viewState).thenReturn(ExpandableViewState())
         ambientState.isSmallScreen = true
@@ -73,6 +77,10 @@
         hostView.addView(notificationRow)
     }
 
+    private fun isTv(): Boolean {
+        return context.packageManager.hasSystemFeature(PackageManager.FEATURE_LEANBACK)
+    }
+
     @Test
     fun resetViewStates_defaultHun_yTranslationIsInset() {
         whenever(notificationRow.isPinned).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index 8545b89..3ad3c15 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -18,6 +18,8 @@
 
 import static android.view.Display.DEFAULT_DISPLAY;
 
+import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION;
+
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
@@ -29,8 +31,10 @@
 import android.app.StatusBarManager;
 import android.os.PowerManager;
 import android.os.UserHandle;
+import android.os.VibrationEffect;
 import android.os.Vibrator;
 import android.testing.AndroidTestingRunner;
+import android.view.HapticFeedbackConstants;
 import android.view.WindowInsets;
 
 import androidx.test.filters.SmallTest;
@@ -42,6 +46,7 @@
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.assist.AssistManager;
+import com.android.systemui.flags.FakeFeatureFlags;
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.qs.QSHost;
@@ -98,6 +103,7 @@
     @Mock private UserTracker mUserTracker;
     @Mock private QSHost mQSHost;
     @Mock private ActivityStarter mActivityStarter;
+    private final FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
 
     CentralSurfacesCommandQueueCallbacks mSbcqCallbacks;
 
@@ -134,7 +140,8 @@
                 mCameraLauncherLazy,
                 mUserTracker,
                 mQSHost,
-                mActivityStarter);
+                mActivityStarter,
+                mFeatureFlags);
 
         when(mUserTracker.getUserHandle()).thenReturn(
                 UserHandle.of(ActivityManager.getCurrentUser()));
@@ -241,4 +248,24 @@
 
         verifyZeroInteractions(mSystemBarAttributesListener);
     }
+
+    @Test
+    public void vibrateOnNavigationKeyDown_oneWayHapticsDisabled_usesVibrate() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
+
+        mSbcqCallbacks.vibrateOnNavigationKeyDown();
+
+        verify(mVibratorHelper).vibrate(VibrationEffect.EFFECT_TICK);
+    }
+
+    @Test
+    public void vibrateOnNavigationKeyDown_oneWayHapticsEnabled_usesPerformHapticFeedback() {
+        mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
+
+        mSbcqCallbacks.vibrateOnNavigationKeyDown();
+
+        verify(mShadeViewController).performHapticFeedback(
+                HapticFeedbackConstants.GESTURE_START
+        );
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt
index b698e70..b78e839 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt
@@ -26,6 +26,7 @@
 import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
@@ -105,6 +106,7 @@
                 systemClock,
                 broadcastDispatcher,
                 shadeExpansionStateManager,
+                mock(),
                 testableHandler,
                 view
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
index 21e4f5a..5f0011b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java
@@ -177,9 +177,13 @@
         mActiveRinger = mDialog.getDialogView().findViewById(
                 R.id.volume_new_ringer_active_icon_container);
         mDrawerContainer = mDialog.getDialogView().findViewById(R.id.volume_drawer_container);
-        mDrawerVibrate = mDrawerContainer.findViewById(R.id.volume_drawer_vibrate);
-        mDrawerMute = mDrawerContainer.findViewById(R.id.volume_drawer_mute);
-        mDrawerNormal = mDrawerContainer.findViewById(R.id.volume_drawer_normal);
+
+        // Drawer is not always available, e.g. on TVs
+        if (mDrawerContainer != null) {
+            mDrawerVibrate = mDrawerContainer.findViewById(R.id.volume_drawer_vibrate);
+            mDrawerMute = mDrawerContainer.findViewById(R.id.volume_drawer_mute);
+            mDrawerNormal = mDrawerContainer.findViewById(R.id.volume_drawer_normal);
+        }
         mODICaptionsIcon = mDialog.getDialogView().findViewById(R.id.odi_captions_icon);
 
         Prefs.putInt(mContext,
@@ -189,6 +193,10 @@
         Prefs.putBoolean(mContext, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false);
     }
 
+    private void assumeHasDrawer() {
+        assumeNotNull("Layout does not contain drawer", mDrawerContainer);
+    }
+
     private State createShellState() {
         State state = new VolumeDialogController.State();
         for (int i = AudioManager.STREAM_VOICE_CALL; i <= AudioManager.STREAM_ACCESSIBILITY; i++) {
@@ -360,6 +368,8 @@
 
     @Test
     public void testSelectVibrateFromDrawer() {
+        assumeHasDrawer();
+
         mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
@@ -375,6 +385,8 @@
 
     @Test
     public void testSelectVibrateFromDrawer_OnewayAPI_On() {
+        assumeHasDrawer();
+
         mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL;
@@ -390,6 +402,8 @@
 
     @Test
     public void testSelectMuteFromDrawer() {
+        assumeHasDrawer();
+
         mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_NORMAL;
@@ -405,6 +419,8 @@
 
     @Test
     public void testSelectMuteFromDrawer_OnewayAPI_On() {
+        assumeHasDrawer();
+
         mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = RINGER_MODE_NORMAL;
@@ -420,6 +436,8 @@
 
     @Test
     public void testSelectNormalFromDrawer() {
+        assumeHasDrawer();
+
         mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, false);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
@@ -435,6 +453,8 @@
 
     @Test
     public void testSelectNormalFromDrawer_OnewayAPI_On() {
+        assumeHasDrawer();
+
         mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true);
         final State initialUnsetState = new State();
         initialUnsetState.ringerModeInternal = AudioManager.RINGER_MODE_VIBRATE;
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
index 492e542..35a3fd0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakeShadeRepository.kt
@@ -30,6 +30,9 @@
     private val _qsExpansion = MutableStateFlow(0f)
     override val qsExpansion = _qsExpansion
 
+    private val _expansion = MutableStateFlow(0f)
+    override val expansion = _expansion
+
     private val _udfpsTransitionToFullShadeProgress = MutableStateFlow(0f)
     override val udfpsTransitionToFullShadeProgress = _udfpsTransitionToFullShadeProgress
 
@@ -41,6 +44,10 @@
         _qsExpansion.value = qsExpansion
     }
 
+    override fun setExpansion(expansion: Float) {
+        _expansion.value = expansion
+    }
+
     override fun setUdfpsTransitionToFullShadeProgress(progress: Float) {
         _udfpsTransitionToFullShadeProgress.value = progress
     }
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
index 0380756..97e5c6f 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsCollector.java
@@ -18,10 +18,14 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.BroadcastReceiver;
 import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
+import android.os.UserHandle;
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
@@ -53,9 +57,21 @@
 
     @NonNull private final Map<Integer, AuthenticationStats> mUserAuthenticationStatsMap;
 
-    @NonNull private AuthenticationStatsPersister mAuthenticationStatsPersister;
+    // TODO(b/295582896): Find a way to make this NonNull
+    @Nullable private AuthenticationStatsPersister mAuthenticationStatsPersister;
     @NonNull private BiometricNotification mBiometricNotification;
 
+    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+            final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
+            if (userId != UserHandle.USER_NULL
+                    && intent.getAction().equals(Intent.ACTION_USER_REMOVED)) {
+                onUserRemoved(userId);
+            }
+        }
+    };
+
     public AuthenticationStatsCollector(@NonNull Context context, int modality,
             @NonNull BiometricNotification biometricNotification) {
         mContext = context;
@@ -64,6 +80,8 @@
         mUserAuthenticationStatsMap = new HashMap<>();
         mModality = modality;
         mBiometricNotification = biometricNotification;
+
+        context.registerReceiver(mBroadcastReceiver, new IntentFilter(Intent.ACTION_USER_REMOVED));
     }
 
     private void initializeUserAuthenticationStatsMap() {
@@ -75,8 +93,9 @@
 
     /** Update total authentication and rejected attempts. */
     public void authenticate(int userId, boolean authenticated) {
-        // Initialize mUserAuthenticationStatsMap when authenticate to ensure SharedPreferences
-        // are ready for application use and avoid ramdump issue.
+        // SharedPreference is not ready when starting system server, initialize
+        // mUserAuthenticationStatsMap in authentication to ensure SharedPreference
+        // is ready for application use.
         if (mUserAuthenticationStatsMap.isEmpty()) {
             initializeUserAuthenticationStatsMap();
         }
@@ -146,6 +165,14 @@
         }
     }
 
+    private void onUserRemoved(final int userId) {
+        if (mAuthenticationStatsPersister == null) {
+            initializeUserAuthenticationStatsMap();
+        }
+        mUserAuthenticationStatsMap.remove(userId);
+        mAuthenticationStatsPersister.removeFrrStats(userId);
+    }
+
     /**
      * Only being used in tests. Callers should not make any changes to the returned
      * authentication stats.
diff --git a/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java
index 96150a6..21e93a8 100644
--- a/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java
+++ b/services/core/java/com/android/server/biometrics/AuthenticationStatsPersister.java
@@ -94,12 +94,35 @@
     }
 
     /**
+     * Remove frr data for a specific user.
+     */
+    public void removeFrrStats(int userId) {
+        try {
+            // Copy into a new HashSet to allow modification.
+            Set<String> frrStatsSet = new HashSet<>(readFrrStats());
+
+            // Remove the old authentication stat for the user if it exists.
+            for (Iterator<String> iterator = frrStatsSet.iterator(); iterator.hasNext();) {
+                String frrStats = iterator.next();
+                JSONObject frrStatJson = new JSONObject(frrStats);
+                if (getValue(frrStatJson, USER_ID).equals(String.valueOf(userId))) {
+                    iterator.remove();
+                    break;
+                }
+            }
+
+            mSharedPreferences.edit().putStringSet(KEY, frrStatsSet).apply();
+        } catch (JSONException ignored) {
+        }
+    }
+
+    /**
      * Persist frr data for a specific user.
      */
     public void persistFrrStats(int userId, int totalAttempts, int rejectedAttempts,
             int enrollmentNotifications, int modality) {
         try {
-            // Copy into a new HashSet to avoid iterator exception.
+            // Copy into a new HashSet to allow modification.
             Set<String> frrStatsSet = new HashSet<>(readFrrStats());
 
             // Remove the old authentication stat for the user if it exists.
diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
index 230ba63..2ff695d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
+++ b/services/core/java/com/android/server/biometrics/sensors/BiometricNotificationUtils.java
@@ -87,11 +87,11 @@
     public static void showFaceEnrollNotification(@NonNull Context context) {
 
         final String name =
-                context.getString(R.string.face_recalibrate_notification_name);
+                context.getString(R.string.device_unlock_notification_name);
         final String title =
-                context.getString(R.string.fingerprint_setup_notification_title);
+                context.getString(R.string.alternative_unlock_setup_notification_title);
         final String content =
-                context.getString(R.string.face_setup_notification_title);
+                context.getString(R.string.alternative_face_setup_notification_content);
 
         final Intent intent = new Intent(FACE_ENROLL_ACTION);
         intent.setPackage(SETTINGS_PACKAGE);
@@ -112,11 +112,11 @@
     public static void showFingerprintEnrollNotification(@NonNull Context context) {
 
         final String name =
-                context.getString(R.string.fingerprint_recalibrate_notification_name);
+                context.getString(R.string.device_unlock_notification_name);
         final String title =
-                context.getString(R.string.fingerprint_setup_notification_title);
+                context.getString(R.string.alternative_unlock_setup_notification_title);
         final String content =
-                context.getString(R.string.fingerprint_setup_notification_content);
+                context.getString(R.string.alternative_fp_setup_notification_content);
 
         final Intent intent = new Intent(FINGERPRINT_ENROLL_ACTION);
         intent.setPackage(SETTINGS_PACKAGE);
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
index 4ad26c4..7ea576d 100644
--- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceController.java
@@ -63,10 +63,15 @@
     // high errors. This default is introduced to provide a fixed display color
     // temperature when sensor readings become unreliable.
     private final float mLowLightAmbientColorTemperature;
+    // As above, but used when in strong mode (idle screen brightness mode).
+    private final float mLowLightAmbientColorTemperatureStrong;
+
     // In high brightness conditions certain color temperatures can cause peak display
     // brightness to drop. This fixed color temperature can be used to compensate for
     // this effect.
     private final float mHighLightAmbientColorTemperature;
+    // As above, but used when in strong mode (idle screen brightness mode).
+    private final float mHighLightAmbientColorTemperatureStrong;
 
     private final boolean mLightModeAllowed;
 
@@ -97,9 +102,11 @@
     // ambient color temperature to the defaults. A piecewise linear relationship
     // between low light brightness and low light bias.
     private Spline.LinearSpline mLowLightAmbientBrightnessToBiasSpline;
+    private Spline.LinearSpline mLowLightAmbientBrightnessToBiasSplineStrong;
 
     // A piecewise linear relationship between high light brightness and high light bias.
     private Spline.LinearSpline mHighLightAmbientBrightnessToBiasSpline;
+    private Spline.LinearSpline mHighLightAmbientBrightnessToBiasSplineStrong;
 
     private float mLatestAmbientColorTemperature;
     private float mLatestAmbientBrightness;
@@ -134,17 +141,29 @@
      * @param lowLightAmbientBrightnesses
      *      The ambient brightness used to map the ambient brightnesses to the biases used to
      *      interpolate to lowLightAmbientColorTemperature.
+     * @param lowLightAmbientBrightnessesStrong
+     *      The ambient brightness used to map the ambient brightnesses to the biases used to
+     *      interpolate to lowLightAmbientColorTemperature.
      * @param lowLightAmbientBiases
      *      The biases used to map the ambient brightnesses to the biases used to interpolate to
      *      lowLightAmbientColorTemperature.
+     * @param lowLightAmbientBiasesStrong
+     *      The biases used to map the ambient brightnesses to the biases used to interpolate to
+     *      lowLightAmbientColorTemperature.
      * @param lowLightAmbientColorTemperature
      *      The ambient color temperature to which we interpolate to based on the low light curve.
      * @param highLightAmbientBrightnesses
      *      The ambient brightness used to map the ambient brightnesses to the biases used to
      *      interpolate to highLightAmbientColorTemperature.
+     * @param highLightAmbientBrightnessesStrong
+     *      The ambient brightness used to map the ambient brightnesses to the biases used to
+     *      interpolate to highLightAmbientColorTemperature.
      * @param highLightAmbientBiases
      *      The biases used to map the ambient brightnesses to the biases used to interpolate to
      *      highLightAmbientColorTemperature.
+     * @param highLightAmbientBiasesStrong
+     *      The biases used to map the ambient brightnesses to the biases used to interpolate to
+     *      highLightAmbientColorTemperature.
      * @param highLightAmbientColorTemperature
      *      The ambient color temperature to which we interpolate to based on the high light curve.
      * @param ambientColorTemperatures
@@ -170,11 +189,17 @@
             @NonNull AmbientFilter colorTemperatureFilter,
             @NonNull DisplayWhiteBalanceThrottler throttler,
             float[] lowLightAmbientBrightnesses,
+            float[] lowLightAmbientBrightnessesStrong,
             float[] lowLightAmbientBiases,
+            float[] lowLightAmbientBiasesStrong,
             float lowLightAmbientColorTemperature,
+            float lowLightAmbientColorTemperatureStrong,
             float[] highLightAmbientBrightnesses,
+            float[] highLightAmbientBrightnessesStrong,
             float[] highLightAmbientBiases,
+            float[] highLightAmbientBiasesStrong,
             float highLightAmbientColorTemperature,
+            float highLightAmbientColorTemperatureStrong,
             float[] ambientColorTemperatures,
             float[] displayColorTemperatures,
             float[] strongAmbientColorTemperatures,
@@ -188,7 +213,9 @@
         mColorTemperatureFilter = colorTemperatureFilter;
         mThrottler = throttler;
         mLowLightAmbientColorTemperature = lowLightAmbientColorTemperature;
+        mLowLightAmbientColorTemperatureStrong = lowLightAmbientColorTemperatureStrong;
         mHighLightAmbientColorTemperature = highLightAmbientColorTemperature;
+        mHighLightAmbientColorTemperatureStrong = highLightAmbientColorTemperatureStrong;
         mAmbientColorTemperature = -1.0f;
         mPendingAmbientColorTemperature = -1.0f;
         mLastAmbientColorTemperature = -1.0f;
@@ -214,6 +241,23 @@
         }
 
         try {
+            mLowLightAmbientBrightnessToBiasSplineStrong = new Spline.LinearSpline(
+                    lowLightAmbientBrightnessesStrong, lowLightAmbientBiasesStrong);
+        } catch (Exception e) {
+            Slog.e(TAG, "failed to create strong low light ambient brightness to bias spline.", e);
+            mLowLightAmbientBrightnessToBiasSplineStrong = null;
+        }
+        if (mLowLightAmbientBrightnessToBiasSplineStrong != null) {
+            if (mLowLightAmbientBrightnessToBiasSplineStrong.interpolate(0.0f) != 0.0f
+                    || mLowLightAmbientBrightnessToBiasSplineStrong.interpolate(
+                    Float.POSITIVE_INFINITY) != 1.0f) {
+                Slog.d(TAG, "invalid strong low light ambient brightness to bias spline, "
+                        + "bias must begin at 0.0 and end at 1.0.");
+                mLowLightAmbientBrightnessToBiasSplineStrong = null;
+            }
+        }
+
+        try {
             mHighLightAmbientBrightnessToBiasSpline = new Spline.LinearSpline(
                     highLightAmbientBrightnesses, highLightAmbientBiases);
         } catch (Exception e) {
@@ -230,6 +274,23 @@
             }
         }
 
+        try {
+            mHighLightAmbientBrightnessToBiasSplineStrong = new Spline.LinearSpline(
+                    highLightAmbientBrightnessesStrong, highLightAmbientBiasesStrong);
+        } catch (Exception e) {
+            Slog.e(TAG, "failed to create strong high light ambient brightness to bias spline.", e);
+            mHighLightAmbientBrightnessToBiasSplineStrong = null;
+        }
+        if (mHighLightAmbientBrightnessToBiasSplineStrong != null) {
+            if (mHighLightAmbientBrightnessToBiasSplineStrong.interpolate(0.0f) != 0.0f
+                    || mHighLightAmbientBrightnessToBiasSplineStrong.interpolate(
+                    Float.POSITIVE_INFINITY) != 1.0f) {
+                Slog.d(TAG, "invalid strong high light ambient brightness to bias spline, "
+                        + "bias must begin at 0.0 and end at 1.0.");
+                mHighLightAmbientBrightnessToBiasSplineStrong = null;
+            }
+        }
+
         if (mLowLightAmbientBrightnessToBiasSpline != null &&
                 mHighLightAmbientBrightnessToBiasSpline != null) {
             if (lowLightAmbientBrightnesses[lowLightAmbientBrightnesses.length - 1] >
@@ -241,6 +302,18 @@
             }
         }
 
+        if (mLowLightAmbientBrightnessToBiasSplineStrong != null
+                && mHighLightAmbientBrightnessToBiasSplineStrong != null) {
+            if (lowLightAmbientBrightnessesStrong[lowLightAmbientBrightnessesStrong.length - 1]
+                    > highLightAmbientBrightnessesStrong[0]) {
+                Slog.d(TAG,
+                        "invalid strong low light and high light ambient brightness to bias "
+                                + "spline combination, defined domains must not intersect.");
+                mLowLightAmbientBrightnessToBiasSplineStrong = null;
+                mHighLightAmbientBrightnessToBiasSplineStrong = null;
+            }
+        }
+
         try {
             mAmbientToDisplayColorTemperatureSpline = new Spline.LinearSpline(
                     ambientColorTemperatures, displayColorTemperatures);
@@ -365,7 +438,11 @@
         mColorTemperatureFilter.dump(writer);
         mThrottler.dump(writer);
         writer.println("  mLowLightAmbientColorTemperature=" + mLowLightAmbientColorTemperature);
+        writer.println("  mLowLightAmbientColorTemperatureStrong="
+                + mLowLightAmbientColorTemperatureStrong);
         writer.println("  mHighLightAmbientColorTemperature=" + mHighLightAmbientColorTemperature);
+        writer.println("  mHighLightAmbientColorTemperatureStrong="
+                + mHighLightAmbientColorTemperatureStrong);
         writer.println("  mAmbientColorTemperature=" + mAmbientColorTemperature);
         writer.println("  mPendingAmbientColorTemperature=" + mPendingAmbientColorTemperature);
         writer.println("  mLastAmbientColorTemperature=" + mLastAmbientColorTemperature);
@@ -377,8 +454,12 @@
                 + mStrongAmbientToDisplayColorTemperatureSpline);
         writer.println("  mLowLightAmbientBrightnessToBiasSpline="
                 + mLowLightAmbientBrightnessToBiasSpline);
+        writer.println("  mLowLightAmbientBrightnessToBiasSplineStrong="
+                + mLowLightAmbientBrightnessToBiasSplineStrong);
         writer.println("  mHighLightAmbientBrightnessToBiasSpline="
                 + mHighLightAmbientBrightnessToBiasSpline);
+        writer.println("  mHighLightAmbientBrightnessToBiasSplineStrong="
+                + mHighLightAmbientBrightnessToBiasSplineStrong);
     }
 
     @Override // AmbientSensor.AmbientBrightnessSensor.Callbacks
@@ -400,6 +481,17 @@
      */
     public void updateAmbientColorTemperature() {
         final long time = System.currentTimeMillis();
+        final float lowLightAmbientColorTemperature = mStrongModeEnabled
+                ? mLowLightAmbientColorTemperatureStrong : mLowLightAmbientColorTemperature;
+        final float highLightAmbientColorTemperature = mStrongModeEnabled
+                ? mHighLightAmbientColorTemperatureStrong : mHighLightAmbientColorTemperature;
+        final Spline.LinearSpline lowLightAmbientBrightnessToBiasSpline = mStrongModeEnabled
+                ? mLowLightAmbientBrightnessToBiasSplineStrong
+                : mLowLightAmbientBrightnessToBiasSpline;
+        final Spline.LinearSpline highLightAmbientBrightnessToBiasSpline = mStrongModeEnabled
+                ? mHighLightAmbientBrightnessToBiasSplineStrong
+                : mHighLightAmbientBrightnessToBiasSpline;
+
         float ambientColorTemperature = mColorTemperatureFilter.getEstimate(time);
         mLatestAmbientColorTemperature = ambientColorTemperature;
 
@@ -423,19 +515,19 @@
         mLatestAmbientBrightness = ambientBrightness;
 
         if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f
-                && mLowLightAmbientBrightnessToBiasSpline != null) {
-            float bias = mLowLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
+                && lowLightAmbientBrightnessToBiasSpline != null) {
+            float bias = lowLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
             ambientColorTemperature =
                     bias * ambientColorTemperature + (1.0f - bias)
-                    * mLowLightAmbientColorTemperature;
+                    * lowLightAmbientColorTemperature;
             mLatestLowLightBias = bias;
         }
         if (ambientColorTemperature != -1.0f && ambientBrightness != -1.0f
-                && mHighLightAmbientBrightnessToBiasSpline != null) {
-            float bias = mHighLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
+                && highLightAmbientBrightnessToBiasSpline != null) {
+            float bias = highLightAmbientBrightnessToBiasSpline.interpolate(ambientBrightness);
             ambientColorTemperature =
                     (1.0f - bias) * ambientColorTemperature + bias
-                    * mHighLightAmbientColorTemperature;
+                    * highLightAmbientColorTemperature;
             mLatestHighLightBias = bias;
         }
 
diff --git a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java
index 62f813f..39e6b3f 100644
--- a/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java
+++ b/services/core/java/com/android/server/display/whitebalance/DisplayWhiteBalanceFactory.java
@@ -70,21 +70,39 @@
         final float[] displayWhiteBalanceLowLightAmbientBrightnesses = getFloatArray(resources,
                 com.android.internal.R.array
                 .config_displayWhiteBalanceLowLightAmbientBrightnesses);
+        final float[] displayWhiteBalanceLowLightAmbientBrightnessesStrong = getFloatArray(
+                resources, com.android.internal.R.array
+                .config_displayWhiteBalanceLowLightAmbientBrightnessesStrong);
         final float[] displayWhiteBalanceLowLightAmbientBiases = getFloatArray(resources,
                 com.android.internal.R.array
                 .config_displayWhiteBalanceLowLightAmbientBiases);
+        final float[] displayWhiteBalanceLowLightAmbientBiasesStrong = getFloatArray(resources,
+                com.android.internal.R.array
+                .config_displayWhiteBalanceLowLightAmbientBiasesStrong);
         final float lowLightAmbientColorTemperature = getFloat(resources,
                 com.android.internal.R.dimen
                 .config_displayWhiteBalanceLowLightAmbientColorTemperature);
+        final float lowLightAmbientColorTemperatureStrong = getFloat(resources,
+                com.android.internal.R.dimen
+                .config_displayWhiteBalanceLowLightAmbientColorTemperatureStrong);
         final float[] displayWhiteBalanceHighLightAmbientBrightnesses = getFloatArray(resources,
                 com.android.internal.R.array
                 .config_displayWhiteBalanceHighLightAmbientBrightnesses);
+        final float[] displayWhiteBalanceHighLightAmbientBrightnessesStrong = getFloatArray(
+                resources, com.android.internal.R.array
+                .config_displayWhiteBalanceHighLightAmbientBrightnessesStrong);
         final float[] displayWhiteBalanceHighLightAmbientBiases = getFloatArray(resources,
                 com.android.internal.R.array
                 .config_displayWhiteBalanceHighLightAmbientBiases);
+        final float[] displayWhiteBalanceHighLightAmbientBiasesStrong = getFloatArray(resources,
+                com.android.internal.R.array
+                .config_displayWhiteBalanceHighLightAmbientBiasesStrong);
         final float highLightAmbientColorTemperature = getFloat(resources,
                 com.android.internal.R.dimen
                 .config_displayWhiteBalanceHighLightAmbientColorTemperature);
+        final float highLightAmbientColorTemperatureStrong = getFloat(resources,
+                com.android.internal.R.dimen
+                .config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong);
         final float[] ambientColorTemperatures = getFloatArray(resources,
                 com.android.internal.R.array.config_displayWhiteBalanceAmbientColorTemperatures);
         final float[] displayColorTemperatures = getFloatArray(resources,
@@ -100,9 +118,15 @@
         final DisplayWhiteBalanceController controller = new DisplayWhiteBalanceController(
                 brightnessSensor, brightnessFilter, colorTemperatureSensor, colorTemperatureFilter,
                 throttler, displayWhiteBalanceLowLightAmbientBrightnesses,
-                displayWhiteBalanceLowLightAmbientBiases, lowLightAmbientColorTemperature,
+                displayWhiteBalanceLowLightAmbientBrightnessesStrong,
+                displayWhiteBalanceLowLightAmbientBiases,
+                displayWhiteBalanceLowLightAmbientBiasesStrong, lowLightAmbientColorTemperature,
+                lowLightAmbientColorTemperatureStrong,
                 displayWhiteBalanceHighLightAmbientBrightnesses,
-                displayWhiteBalanceHighLightAmbientBiases, highLightAmbientColorTemperature,
+                displayWhiteBalanceHighLightAmbientBrightnessesStrong,
+                displayWhiteBalanceHighLightAmbientBiases,
+                displayWhiteBalanceHighLightAmbientBiasesStrong, highLightAmbientColorTemperature,
+                highLightAmbientColorTemperatureStrong,
                 ambientColorTemperatures, displayColorTemperatures, strongAmbientColorTemperatures,
                 strongDisplayColorTemperatures, lightModeAllowed);
         brightnessSensor.setCallbacks(controller);
diff --git a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
index d764ec4..9172dc0 100644
--- a/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
+++ b/services/core/java/com/android/server/hdmi/AbsoluteVolumeAudioStatusAction.java
@@ -32,6 +32,10 @@
 
     private int mInitialAudioStatusRetriesLeft = 2;
 
+    // Flag to notify AudioService of the next audio status reported,
+    // regardless of whether the audio status changed.
+    private boolean mForceNextAudioStatusUpdate = false;
+
     private static final int STATE_WAIT_FOR_INITIAL_AUDIO_STATUS = 1;
     private static final int STATE_MONITOR_AUDIO_STATUS = 2;
 
@@ -70,6 +74,17 @@
         return false;
     }
 
+
+    /**
+     * If AVB has been enabled, send <Give Audio Status> and notify AudioService of the response.
+     */
+    void requestAndUpdateAudioStatus() {
+        if (mState == STATE_MONITOR_AUDIO_STATUS) {
+            mForceNextAudioStatusUpdate = true;
+            sendGiveAudioStatus();
+        }
+    }
+
     private boolean handleReportAudioStatus(HdmiCecMessage cmd) {
         if (mTargetAddress != cmd.getSource() || cmd.getParams().length == 0) {
             return false;
@@ -89,12 +104,15 @@
             localDevice().getService().enableAbsoluteVolumeBehavior(audioStatus);
             mState = STATE_MONITOR_AUDIO_STATUS;
         } else if (mState == STATE_MONITOR_AUDIO_STATUS) {
-            if (audioStatus.getVolume() != mLastAudioStatus.getVolume()) {
+            if (mForceNextAudioStatusUpdate
+                    || audioStatus.getVolume() != mLastAudioStatus.getVolume()) {
                 localDevice().getService().notifyAvbVolumeChange(audioStatus.getVolume());
             }
-            if (audioStatus.getMute() != mLastAudioStatus.getMute()) {
+            if (mForceNextAudioStatusUpdate
+                    || audioStatus.getMute() != mLastAudioStatus.getMute()) {
                 localDevice().getService().notifyAvbMuteChange(audioStatus.getMute());
             }
+            mForceNextAudioStatusUpdate = false;
         }
         mLastAudioStatus = audioStatus;
 
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index 207d38e..0671464 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -1048,6 +1048,19 @@
     }
 
     /**
+     * If AVB has been enabled, request the System Audio device's audio status and notify
+     * AudioService of its response.
+     */
+    @ServiceThreadOnly
+    void requestAndUpdateAvbAudioStatus() {
+        assertRunOnServiceThread();
+        for (AbsoluteVolumeAudioStatusAction action :
+                getActions(AbsoluteVolumeAudioStatusAction.class)) {
+            action.requestAndUpdateAudioStatus();
+        }
+    }
+
+    /**
      * Determines whether {@code targetAddress} supports <Set Audio Volume Level>. Does two things
      * in parallel: send <Give Features> (to get <Report Features> in response),
      * and send <Set Audio Volume Level> (to see if it gets a <Feature Abort> in response).
diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java
index 5abb2b5..99fa3a3 100644
--- a/services/core/java/com/android/server/hdmi/HdmiControlService.java
+++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java
@@ -4661,6 +4661,13 @@
                     // same keycode for all three mute options.
                     keyCode = KeyEvent.KEYCODE_VOLUME_MUTE;
                     break;
+                case AudioManager.ADJUST_SAME:
+                    // Query the current audio status of the Audio System and display UI for it
+                    // Only for TVs, because Playback devices don't display UI when using AVB
+                    if (tv() != null) {
+                        tv().requestAndUpdateAvbAudioStatus();
+                    }
+                    return;
                 default:
                     return;
             }
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 2fc4829..8e7baf2 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -5959,6 +5959,8 @@
             mVisibilityStateComputer.dump(pw);
             p.println("  mInFullscreenMode=" + mInFullscreenMode);
             p.println("  mSystemReady=" + mSystemReady + " mInteractive=" + mIsInteractive);
+            p.println("  ENABLE_HIDE_IME_CAPTION_BAR="
+                    + InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR);
             p.println("  mSettingsObserver=" + mSettingsObserver);
             p.println("  mStylusIds=" + (mStylusIds != null
                     ? Arrays.toString(mStylusIds.toArray()) : ""));
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 3a20cd9..f2242bf 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -2188,11 +2188,10 @@
             MediaSessionRecordImpl session = isGlobalPriorityActiveLocked() ? mGlobalPrioritySession
                     : mCurrentFullUserRecord.mPriorityStack.getDefaultVolumeSession();
 
-            boolean preferSuggestedStream = false;
-            if (isValidLocalStreamType(suggestedStream)
-                    && AudioSystem.isStreamActive(suggestedStream, 0)) {
-                preferSuggestedStream = true;
-            }
+            boolean preferSuggestedStream =
+                    isValidLocalStreamType(suggestedStream)
+                            && AudioSystem.isStreamActive(suggestedStream, 0);
+
             if (session == null || preferSuggestedStream) {
                 if (DEBUG_KEY_EVENT) {
                     Log.d(TAG, "Adjusting suggestedStream=" + suggestedStream + " by " + direction
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index b2d3fca..100c638 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -25,8 +25,6 @@
 import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_POSITIVE;
 
 import android.annotation.Nullable;
-import android.app.ActivityManager;
-import android.app.IActivityManager;
 import android.app.KeyguardManager;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -48,6 +46,7 @@
 import android.os.Bundle;
 import android.os.IBinder;
 import android.os.PowerManager;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.os.VibrationEffect;
 import android.provider.Settings;
@@ -98,8 +97,7 @@
     // the period after which a notification is updated where it can make sound
     private static final int MAX_SOUND_DELAY_MS = 2000;
     private final StatusBarNotification sbn;
-    IActivityManager mAm;
-    UriGrantsManagerInternal mUgmInternal;
+    private final UriGrantsManagerInternal mUgmInternal;
     final int mTargetSdkVersion;
     final int mOriginalFlags;
     private final Context mContext;
@@ -223,7 +221,6 @@
         this.sbn = sbn;
         mTargetSdkVersion = LocalServices.getService(PackageManagerInternal.class)
                 .getPackageTargetSdkVersion(sbn.getPackageName());
-        mAm = ActivityManager.getService();
         mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
         mOriginalFlags = sbn.getNotification().flags;
         mRankingTimeMs = calculateRankingTimeMs(0L);
@@ -1387,18 +1384,27 @@
      * Collect all {@link Uri} that should have permission granted to whoever
      * will be rendering it.
      */
-    protected void calculateGrantableUris() {
-        final Notification notification = getNotification();
-        notification.visitUris((uri) -> {
-            visitGrantableUri(uri, false, false);
-        });
+    private void calculateGrantableUris() {
+        Trace.beginSection("NotificationRecord.calculateGrantableUris");
+        try {
+            // We can't grant URI permissions from system.
+            final int sourceUid = getSbn().getUid();
+            if (sourceUid == android.os.Process.SYSTEM_UID) return;
 
-        if (notification.getChannelId() != null) {
-            NotificationChannel channel = getChannel();
-            if (channel != null) {
-                visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
-                        & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+            final Notification notification = getNotification();
+            notification.visitUris((uri) -> {
+                visitGrantableUri(uri, false, false);
+            });
+
+            if (notification.getChannelId() != null) {
+                NotificationChannel channel = getChannel();
+                if (channel != null) {
+                    visitGrantableUri(channel.getSound(), (channel.getUserLockedFields()
+                            & NotificationChannel.USER_LOCKED_SOUND) != 0, true);
+                }
             }
+        } finally {
+            Trace.endSection();
         }
     }
 
@@ -1413,13 +1419,14 @@
     private void visitGrantableUri(Uri uri, boolean userOverriddenUri, boolean isSound) {
         if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return;
 
-        // We can't grant Uri permissions from system
-        final int sourceUid = getSbn().getUid();
-        if (sourceUid == android.os.Process.SYSTEM_UID) return;
+        if (mGrantableUris != null && mGrantableUris.contains(uri)) {
+            return; // already verified this URI
+        }
 
+        final int sourceUid = getSbn().getUid();
         final long ident = Binder.clearCallingIdentity();
         try {
-            // This will throw SecurityException if caller can't grant
+            // This will throw a SecurityException if the caller can't grant.
             mUgmInternal.checkGrantUriPermission(sourceUid, null,
                     ContentProvider.getUriWithoutUserId(uri),
                     Intent.FLAG_GRANT_READ_URI_PERMISSION,
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 385dfcb8..f2797eb 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -4907,6 +4907,11 @@
                                 USER_OPERATION_ERROR_UNKNOWN);
                     }
                 }
+                if (isMainUser && getMainUserIdUnchecked() != UserHandle.USER_NULL) {
+                    throwCheckedUserOperationException(
+                            "Cannot add user with FLAG_MAIN as main user already exists.",
+                            UserManager.USER_OPERATION_ERROR_MAX_USERS);
+                }
                 if (!preCreate && !canAddMoreUsersOfType(userTypeDetails)) {
                     throwCheckedUserOperationException(
                             "Cannot add more users of type " + userType
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 3125518..cba215a 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -224,6 +224,9 @@
 import static com.android.server.wm.IdentifierProto.USER_ID;
 import static com.android.server.wm.LetterboxConfiguration.DEFAULT_LETTERBOX_ASPECT_RATIO_FOR_MULTI_WINDOW;
 import static com.android.server.wm.LetterboxConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO;
+import static com.android.server.wm.StartingData.AFTER_TRANSACTION_COPY_TO_CLIENT;
+import static com.android.server.wm.StartingData.AFTER_TRANSACTION_IDLE;
+import static com.android.server.wm.StartingData.AFTER_TRANSACTION_REMOVE_DIRECTLY;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_RECENTS;
@@ -2690,6 +2693,11 @@
         if (isTransferringSplashScreen()) {
             return true;
         }
+        // Only do transfer after transaction has done when starting window exist.
+        if (mStartingData != null && mStartingData.mWaitForSyncTransactionCommit) {
+            mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_COPY_TO_CLIENT;
+            return true;
+        }
         requestCopySplashScreen();
         return isTransferringSplashScreen();
     }
@@ -2850,11 +2858,14 @@
         if (mStartingData == null) {
             return;
         }
-        mStartingData.mWaitForSyncTransactionCommit = false;
-        if (mStartingData.mRemoveAfterTransaction) {
-            mStartingData.mRemoveAfterTransaction = false;
-            removeStartingWindowAnimation(mStartingData.mPrepareRemoveAnimation);
+        final StartingData lastData = mStartingData;
+        lastData.mWaitForSyncTransactionCommit = false;
+        if (lastData.mRemoveAfterTransaction == AFTER_TRANSACTION_REMOVE_DIRECTLY) {
+            removeStartingWindowAnimation(lastData.mPrepareRemoveAnimation);
+        } else if (lastData.mRemoveAfterTransaction == AFTER_TRANSACTION_COPY_TO_CLIENT) {
+            removeStartingWindow();
         }
+        lastData.mRemoveAfterTransaction = AFTER_TRANSACTION_IDLE;
     }
 
     void removeStartingWindowAnimation(boolean prepareAnimation) {
@@ -2881,7 +2892,7 @@
         if (mStartingData != null) {
             if (mStartingData.mWaitForSyncTransactionCommit
                     || mTransitionController.inCollectingTransition(startingWindow)) {
-                mStartingData.mRemoveAfterTransaction = true;
+                mStartingData.mRemoveAfterTransaction = AFTER_TRANSACTION_REMOVE_DIRECTLY;
                 mStartingData.mPrepareRemoveAnimation = prepareAnimation;
                 return;
             }
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index 34806bd..a23547e 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -16,6 +16,8 @@
 
 package com.android.server.wm;
 
+import android.annotation.IntDef;
+
 import com.android.server.wm.StartingSurfaceController.StartingSurface;
 
 /**
@@ -23,6 +25,20 @@
  */
 public abstract class StartingData {
 
+    /** Nothing need to do after transaction */
+    static final int AFTER_TRANSACTION_IDLE = 0;
+    /** Remove the starting window directly after transaction done. */
+    static final int AFTER_TRANSACTION_REMOVE_DIRECTLY = 1;
+    /** Do copy splash screen to client after transaction done. */
+    static final int AFTER_TRANSACTION_COPY_TO_CLIENT = 2;
+
+    @IntDef(prefix = { "AFTER_TRANSACTION" }, value = {
+            AFTER_TRANSACTION_IDLE,
+            AFTER_TRANSACTION_REMOVE_DIRECTLY,
+            AFTER_TRANSACTION_COPY_TO_CLIENT,
+    })
+    @interface AfterTransaction {}
+
     protected final WindowManagerService mService;
     protected final int mTypeParams;
 
@@ -60,7 +76,7 @@
      * This starting window should be removed after applying the start transaction of transition,
      * which ensures the app window has shown.
      */
-    boolean mRemoveAfterTransaction;
+    @AfterTransaction int mRemoveAfterTransaction;
 
     /** Whether to prepare the removal animation. */
     boolean mPrepareRemoveAnimation;
diff --git a/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java b/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java
index f975b6f..183a84d 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/whitebalance/AmbientLuxTest.java
@@ -64,7 +64,9 @@
     private static final int AMBIENT_COLOR_TYPE = 20705;
     private static final String AMBIENT_COLOR_TYPE_STR = "colorSensoryDensoryDoc";
     private static final float LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE = 5432.1f;
+    private static final float LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG = 5555.5f;
     private static final float HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE = 3456.7f;
+    private static final float HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG = 3333.3f;
 
     private Handler mHandler = new Handler(Looper.getMainLooper());
     private Sensor mLightSensor;
@@ -78,6 +80,10 @@
     @Mock private TypedArray mBiases;
     @Mock private TypedArray mHighLightBrightnesses;
     @Mock private TypedArray mHighLightBiases;
+    @Mock private TypedArray mBrightnessesStrong;
+    @Mock private TypedArray mBiasesStrong;
+    @Mock private TypedArray mHighLightBrightnessesStrong;
+    @Mock private TypedArray mHighLightBiasesStrong;
     @Mock private TypedArray mAmbientColorTemperatures;
     @Mock private TypedArray mDisplayColorTemperatures;
     @Mock private TypedArray mStrongAmbientColorTemperatures;
@@ -108,6 +114,10 @@
                 LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE);
         mockResourcesFloat(R.dimen.config_displayWhiteBalanceHighLightAmbientColorTemperature,
                 HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE);
+        mockResourcesFloat(R.dimen.config_displayWhiteBalanceLowLightAmbientColorTemperatureStrong,
+                LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG);
+        mockResourcesFloat(R.dimen.config_displayWhiteBalanceHighLightAmbientColorTemperatureStrong,
+                HIGH_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG);
         when(mResourcesSpy.obtainTypedArray(
                 R.array.config_displayWhiteBalanceAmbientColorTemperatures))
                 .thenReturn(mAmbientColorTemperatures);
@@ -133,6 +143,18 @@
         when(mResourcesSpy.obtainTypedArray(
                 R.array.config_displayWhiteBalanceHighLightAmbientBiases))
                 .thenReturn(mHighLightBiases);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceLowLightAmbientBrightnessesStrong))
+                .thenReturn(mBrightnessesStrong);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceLowLightAmbientBiasesStrong))
+                .thenReturn(mBiasesStrong);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceHighLightAmbientBrightnessesStrong))
+                .thenReturn(mHighLightBrightnessesStrong);
+        when(mResourcesSpy.obtainTypedArray(
+                R.array.config_displayWhiteBalanceHighLightAmbientBiasesStrong))
+                .thenReturn(mHighLightBiasesStrong);
         mockThrottler();
         LocalServices.removeServiceForTest(ColorDisplayService.ColorDisplayServiceInternal.class);
         LocalServices.addService(ColorDisplayService.ColorDisplayServiceInternal.class,
@@ -388,8 +410,8 @@
     public void testStrongMode() {
         final float lowerBrightness = 10.0f;
         final float upperBrightness = 50.0f;
-        setBrightnesses(lowerBrightness, upperBrightness);
-        setBiases(0.0f, 1.0f);
+        setBrightnessesStrong(lowerBrightness, upperBrightness);
+        setBiasesStrong(0.0f, 1.0f);
         final int ambientColorTempLow = 6000;
         final int ambientColorTempHigh = 8000;
         final int displayColorTempLow = 6400;
@@ -413,7 +435,7 @@
                 setEstimatedBrightnessAndUpdate(controller,
                         mix(lowerBrightness, upperBrightness, brightnessFraction));
                 assertEquals(controller.mPendingAmbientColorTemperature,
-                        mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE,
+                        mix(LOW_LIGHT_AMBIENT_COLOR_TEMPERATURE_STRONG,
                                 mix(displayColorTempLow, displayColorTempHigh, ambientTempFraction),
                                 brightnessFraction),
                         ALLOWED_ERROR_DELTA);
@@ -458,7 +480,7 @@
         assertEquals(-1.0f, controller.mPendingAmbientColorTemperature, 0);
     }
 
-    void mockThrottler() {
+    private void mockThrottler() {
         when(mResourcesSpy.getInteger(
                 R.integer.config_displayWhiteBalanceDecreaseDebounce)).thenReturn(0);
         when(mResourcesSpy.getInteger(
@@ -513,10 +535,18 @@
         setFloatArrayResource(mBrightnesses, vals);
     }
 
+    private void setBrightnessesStrong(float... vals) {
+        setFloatArrayResource(mBrightnessesStrong, vals);
+    }
+
     private void setBiases(float... vals) {
         setFloatArrayResource(mBiases, vals);
     }
 
+    private void setBiasesStrong(float... vals) {
+        setFloatArrayResource(mBiasesStrong, vals);
+    }
+
     private void setHighLightBrightnesses(float... vals) {
         setFloatArrayResource(mHighLightBrightnesses, vals);
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
index 78e5a42..bdbf4ec 100644
--- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java
@@ -63,6 +63,8 @@
 import org.junit.Test;
 import org.mockito.Mock;
 
+import java.io.File;
+
 /**
  * Run as {@code atest FrameworksMockingServicesTests:com.android.server.pm.UserManagerServiceTest}
  */
@@ -107,6 +109,8 @@
             .getTargetContext();
     private final SparseArray<UserData> mUsers = new SparseArray<>();
 
+    private File mTestDir;
+
     private Context mSpiedContext;
 
     private @Mock PackageManagerService mMockPms;
@@ -144,17 +148,23 @@
         doNothing().when(mSpiedContext).sendBroadcastAsUser(any(), any(), any());
 
         // Must construct UserManagerService in the UiThread
+        mTestDir = new File(mRealContext.getDataDir(), "umstest");
+        mTestDir.mkdirs();
         mUms = new UserManagerService(mSpiedContext, mMockPms, mMockUserDataPreparer,
-                mPackagesLock, mRealContext.getDataDir(), mUsers);
+                mPackagesLock, mTestDir, mUsers);
         mUmi = LocalServices.getService(UserManagerInternal.class);
         assertWithMessage("LocalServices.getService(UserManagerInternal.class)").that(mUmi)
                 .isNotNull();
     }
 
     @After
-    public void resetUserManagerInternal() {
+    public void tearDown() {
         // LocalServices follows the "Highlander rule" - There can be only one!
         LocalServices.removeServiceForTest(UserManagerInternal.class);
+
+        // Clean up test dir to remove persisted user files.
+        assertThat(deleteRecursive(mTestDir)).isTrue();
+        mUsers.clear();
     }
 
     @Test
@@ -496,6 +506,14 @@
 
     @Test
     public void testMainUser_hasNoCallsOrSMSRestrictionsByDefault() {
+        // Remove the main user so we can add another one
+        for (int i = 0; i < mUsers.size(); i++) {
+            UserData userData = mUsers.valueAt(i);
+            if (userData.info.isMain()) {
+                mUsers.delete(i);
+                break;
+            }
+        }
         UserInfo mainUser = mUms.createUserWithThrow("main user", USER_TYPE_FULL_SECONDARY,
                 UserInfo.FLAG_FULL | UserInfo.FLAG_MAIN);
 
@@ -621,6 +639,18 @@
         userData.mLastEnteredForegroundTimeMillis = timeMillis;
     }
 
+    public boolean deleteRecursive(File file) {
+        if (file.isDirectory()) {
+            for (File item : file.listFiles()) {
+                boolean success = deleteRecursive(item);
+                if (!success) {
+                    return false;
+                }
+            }
+        }
+        return file.delete();
+    }
+
     private static final class TestUserData extends UserData {
 
         @SuppressWarnings("deprecation")
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
index 285a20c..746fb53 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsCollectorTest.java
@@ -36,6 +36,9 @@
 import android.content.res.Resources;
 import android.hardware.face.FaceManager;
 import android.hardware.fingerprint.FingerprintManager;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
 
 import com.android.internal.R;
 import com.android.server.biometrics.sensors.BiometricNotification;
@@ -47,6 +50,8 @@
 
 import java.io.File;
 
+@Presubmit
+@SmallTest
 public class AuthenticationStatsCollectorTest {
 
     private AuthenticationStatsCollector mAuthenticationStatsCollector;
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java
index 455625c..dde2a3c 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthenticationStatsPersisterTest.java
@@ -29,6 +29,9 @@
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.hardware.biometrics.BiometricsProtoEnums;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
 
 import org.json.JSONException;
 import org.json.JSONObject;
@@ -45,6 +48,8 @@
 import java.util.List;
 import java.util.Set;
 
+@Presubmit
+@SmallTest
 public class AuthenticationStatsPersisterTest {
 
     @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
@@ -211,6 +216,20 @@
         assertThat(mStringSetArgumentCaptor.getValue()).contains(expectedFrrStats);
     }
 
+    @Test
+    public void removeFrrStats_existingUser_shouldUpdateRecord() throws JSONException {
+        AuthenticationStats authenticationStats = new AuthenticationStats(USER_ID_1,
+                300 /* totalAttempts */, 10 /* rejectedAttempts */,
+                0 /* enrollmentNotifications */, BiometricsProtoEnums.MODALITY_FACE);
+        when(mSharedPreferences.getStringSet(eq(KEY), anySet())).thenReturn(
+                Set.of(buildFrrStats(authenticationStats)));
+
+        mAuthenticationStatsPersister.removeFrrStats(USER_ID_1);
+
+        verify(mEditor).putStringSet(eq(KEY), mStringSetArgumentCaptor.capture());
+        assertThat(mStringSetArgumentCaptor.getValue()).doesNotContain(authenticationStats);
+    }
+
     private String buildFrrStats(AuthenticationStats authenticationStats)
             throws JSONException {
         if (authenticationStats.getModality() == BiometricsProtoEnums.MODALITY_FACE) {
diff --git a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
index e9a7d85..0376376 100644
--- a/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
+++ b/services/tests/servicestests/src/com/android/server/graphics/fonts/UpdatableFontDirTest.java
@@ -67,6 +67,8 @@
 @RunWith(AndroidJUnit4.class)
 public final class UpdatableFontDirTest {
 
+    private static final String LEGACY_FONTS_XML = "/system/etc/fonts.xml";
+
     /**
      * A {@link UpdatableFontDir.FontFileParser} for testing. Instead of using real font files,
      * this test uses fake font files. A fake font file has its PostScript naem and revision as the
@@ -140,7 +142,7 @@
     private List<File> mPreinstalledFontDirs;
     private final Supplier<Long> mCurrentTimeSupplier = () -> CURRENT_TIME;
     private final Function<Map<String, File>, FontConfig> mConfigSupplier =
-            (map) -> SystemFonts.getSystemFontConfig(map, 0, 0);
+            (map) -> SystemFonts.getSystemFontConfigForTesting(LEGACY_FONTS_XML, map, 0, 0);
     private FakeFontFileParser mParser;
     private FakeFsverityUtil mFakeFsverityUtil;
 
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
index 399655ffa..e6d326a 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/BaseAbsoluteVolumeBehaviorTest.java
@@ -92,11 +92,11 @@
 
     // Default Audio Status given by the System Audio device in its initial <Report Audio Status>
     // that triggers AVB being enabled
-    private static final AudioStatus INITIAL_SYSTEM_AUDIO_DEVICE_STATUS =
+    protected static final AudioStatus INITIAL_SYSTEM_AUDIO_DEVICE_STATUS =
             new AudioStatus(50, false);
 
     // VolumeInfo passed to AudioDeviceVolumeManager#setDeviceAbsoluteVolumeBehavior to enable AVB
-    private static final VolumeInfo ENABLE_AVB_VOLUME_INFO =
+    protected static final VolumeInfo ENABLE_AVB_VOLUME_INFO =
             new VolumeInfo.Builder(AudioManager.STREAM_MUSIC)
                     .setMuted(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute())
                     .setVolumeIndex(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume())
@@ -106,6 +106,8 @@
 
     private static final int EMPTY_FLAGS = 0;
 
+    protected static final int STREAM_MUSIC_MAX_VOLUME = 25;
+
     protected abstract HdmiCecLocalDevice createLocalDevice(HdmiControlService hdmiControlService);
 
     protected abstract int getPhysicalAddress();
@@ -201,7 +203,7 @@
                 Collections.singletonList(getAudioOutputDevice()));
 
         // Max volume of STREAM_MUSIC
-        mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, 25);
+        mAudioFramework.setStreamMaxVolume(AudioManager.STREAM_MUSIC, STREAM_MUSIC_MAX_VOLUME);
 
         // Receive messages from devices to make sure they're registered in HdmiCecNetwork
         mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildGiveDevicePowerStatus(
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java
index 024e36d..86647fc 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/TvToAudioSystemAvbTest.java
@@ -321,4 +321,98 @@
                         getLogicalAddress(), getSystemAudioDeviceLogicalAddress(),
                         Constants.AUDIO_VOLUME_STATUS_UNKNOWN));
     }
+
+    /**
+     * Tests that a volume adjustment command with direction ADJUST_SAME causes HdmiControlService
+     * to request the System Audio device's audio status, and notify AudioService of the
+     * audio status.
+     */
+    @Test
+    public void avbEnabled_audioDeviceVolumeAdjusted_adjustSame_updatesAudioService() {
+        enableAbsoluteVolumeBehavior();
+        mNativeWrapper.clearResultMessages();
+
+        // HdmiControlService receives a volume adjustment with direction ADJUST_SAME
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted(
+                getAudioOutputDevice(),
+                ENABLE_AVB_VOLUME_INFO,
+                AudioManager.ADJUST_SAME,
+                AudioDeviceVolumeManager.ADJUST_MODE_NORMAL
+        );
+        mTestLooper.dispatchAll();
+
+        // Device sends <Give Audio Status>
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress()));
+
+        clearInvocations(mAudioManager);
+
+        // Device receives <Report Audio Status> with a new volume and mute state
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus(
+                getSystemAudioDeviceLogicalAddress(),
+                getLogicalAddress(),
+                80,
+                true));
+        mTestLooper.dispatchAll();
+
+        // HdmiControlService calls setStreamVolume and adjustStreamVolume to trigger volume UI
+        verify(mAudioManager).setStreamVolume(
+                eq(AudioManager.STREAM_MUSIC),
+                // Volume level is rescaled to the max volume of STREAM_MUSIC
+                eq(80 * STREAM_MUSIC_MAX_VOLUME / AudioStatus.MAX_VOLUME),
+                eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI));
+        verify(mAudioManager).adjustStreamVolume(
+                eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_MUTE),
+                eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI));
+    }
+
+    /**
+     * Tests that a volume adjustment command with direction ADJUST_SAME causes HdmiControlService
+     * to request the System Audio device's audio status, and notify AudioService of the
+     * audio status, even if it's unchanged from the previous one.
+     */
+    @Test
+    public void avbEnabled_audioDeviceVolumeAdjusted_adjustSame_noChange_updatesAudioService() {
+        enableAbsoluteVolumeBehavior();
+        mNativeWrapper.clearResultMessages();
+
+        // HdmiControlService receives a volume adjustment with direction ADJUST_SAME
+        mHdmiControlService.getAbsoluteVolumeChangedListener().onAudioDeviceVolumeAdjusted(
+                getAudioOutputDevice(),
+                ENABLE_AVB_VOLUME_INFO,
+                AudioManager.ADJUST_SAME,
+                AudioDeviceVolumeManager.ADJUST_MODE_NORMAL
+        );
+        mTestLooper.dispatchAll();
+
+        // Device sends <Give Audio Status>
+        assertThat(mNativeWrapper.getResultMessages()).contains(
+                HdmiCecMessageBuilder.buildGiveAudioStatus(getLogicalAddress(),
+                        getSystemAudioDeviceLogicalAddress()));
+
+        clearInvocations(mAudioManager);
+
+        // Device receives <Report Audio Status> with the same volume level and mute state that
+        // as when AVB was enabled
+        mNativeWrapper.onCecMessage(HdmiCecMessageBuilder.buildReportAudioStatus(
+                getSystemAudioDeviceLogicalAddress(),
+                getLogicalAddress(),
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume(),
+                INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getMute()));
+        mTestLooper.dispatchAll();
+
+        // HdmiControlService calls setStreamVolume and adjustStreamVolume to trigger volume UI
+        verify(mAudioManager).setStreamVolume(
+                eq(AudioManager.STREAM_MUSIC),
+                // Volume level is rescaled to the max volume of STREAM_MUSIC
+                eq(INITIAL_SYSTEM_AUDIO_DEVICE_STATUS.getVolume()
+                        * STREAM_MUSIC_MAX_VOLUME / AudioStatus.MAX_VOLUME),
+                eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI));
+        verify(mAudioManager).adjustStreamVolume(
+                eq(AudioManager.STREAM_MUSIC),
+                eq(AudioManager.ADJUST_UNMUTE),
+                eq(AudioManager.FLAG_ABSOLUTE_VOLUME | AudioManager.FLAG_SHOW_UI));
+    }
 }
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index cb659b6..ecd35a5 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -1565,6 +1565,25 @@
         assertThat(userInfo.name).isEqualTo(newName);
     }
 
+    @Test
+    public void testCannotCreateAdditionalMainUser() {
+        UserHandle mainUser = mUserManager.getMainUser();
+        assumeTrue("There is no main user", mainUser != null);
+
+        // Users with FLAG_MAIN can't be removed, so no point using the local createUser method.
+        UserInfo newMainUser = mUserManager.createUser("test", UserInfo.FLAG_MAIN);
+        assertThat(newMainUser).isNull();
+
+        List<UserInfo> users = mUserManager.getUsers();
+        int mainUserCount = 0;
+        for (UserInfo user : users) {
+            if (user.isMain()) {
+                mainUserCount++;
+            }
+        }
+        assertThat(mainUserCount).isEqualTo(1);
+    }
+
     private boolean isPackageInstalledForUser(String packageName, int userId) {
         try {
             return mPackageManager.getPackageInfoAsUser(packageName, 0, userId) != null;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index fae92d9..f83a1df 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -36,7 +36,7 @@
 import static junit.framework.Assert.assertTrue;
 
 import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -44,7 +44,6 @@
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
-import android.app.IActivityManager;
 import android.app.Notification;
 import android.app.Notification.Builder;
 import android.app.NotificationChannel;
@@ -76,6 +75,7 @@
 
 import com.android.internal.R;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.server.LocalServices;
 import com.android.server.UiServiceTestCase;
 import com.android.server.uri.UriGrantsManagerInternal;
 
@@ -850,84 +850,78 @@
 
     @Test
     public void testCalculateGrantableUris_PappProvided() {
-        IActivityManager am = mock(IActivityManager.class);
         UriGrantsManagerInternal ugm = mock(UriGrantsManagerInternal.class);
         when(ugm.checkGrantUriPermission(anyInt(), eq(null), any(Uri.class),
                 anyInt(), anyInt())).thenThrow(new SecurityException());
 
+        LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
+        LocalServices.addService(UriGrantsManagerInternal.class, ugm);
+
         channel.setSound(null, null);
         Notification n = new Notification.Builder(mContext, channel.getId())
                 .setSmallIcon(Icon.createWithContentUri(Uri.parse("content://something")))
                 .build();
         StatusBarNotification sbn =
                 new StatusBarNotification(PKG_P, PKG_P, id1, tag1, uid, uid, n, mUser, null, uid);
-        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
-        record.mAm = am;
-        record.mUgmInternal = ugm;
 
-        try {
-            record.calculateGrantableUris();
-            fail("App provided uri for p targeting app should throw exception");
-        } catch (SecurityException e) {
-            // expected
-        }
+        assertThrows("App provided uri for p targeting app should throw exception",
+                SecurityException.class,
+                () -> new NotificationRecord(mMockContext, sbn, channel));
     }
 
     @Test
     public void testCalculateGrantableUris_PappProvided_invalidSound() {
-        IActivityManager am = mock(IActivityManager.class);
         UriGrantsManagerInternal ugm = mock(UriGrantsManagerInternal.class);
         when(ugm.checkGrantUriPermission(anyInt(), eq(null), any(Uri.class),
                 anyInt(), anyInt())).thenThrow(new SecurityException());
 
+        LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
+        LocalServices.addService(UriGrantsManagerInternal.class, ugm);
+
         channel.setSound(Uri.parse("content://something"), mock(AudioAttributes.class));
 
         Notification n = mock(Notification.class);
         when(n.getChannelId()).thenReturn(channel.getId());
         StatusBarNotification sbn =
                 new StatusBarNotification(PKG_P, PKG_P, id1, tag1, uid, uid, n, mUser, null, uid);
-        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
-        record.mAm = am;
-        record.mUgmInternal = ugm;
 
-        record.calculateGrantableUris();
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
         assertEquals(Settings.System.DEFAULT_NOTIFICATION_URI, record.getSound());
     }
 
     @Test
     public void testCalculateGrantableUris_PuserOverridden() {
-        IActivityManager am = mock(IActivityManager.class);
         UriGrantsManagerInternal ugm = mock(UriGrantsManagerInternal.class);
         when(ugm.checkGrantUriPermission(anyInt(), eq(null), any(Uri.class),
                 anyInt(), anyInt())).thenThrow(new SecurityException());
 
+        LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
+        LocalServices.addService(UriGrantsManagerInternal.class, ugm);
+
         channel.lockFields(NotificationChannel.USER_LOCKED_SOUND);
         Notification n = mock(Notification.class);
         when(n.getChannelId()).thenReturn(channel.getId());
         StatusBarNotification sbn =
                 new StatusBarNotification(PKG_P, PKG_P, id1, tag1, uid, uid, n, mUser, null, uid);
-        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
-        record.mAm = am;
 
-        record.calculateGrantableUris();
+        new NotificationRecord(mMockContext, sbn, channel); // should not throw
     }
 
     @Test
     public void testCalculateGrantableUris_prePappProvided() {
-        IActivityManager am = mock(IActivityManager.class);
         UriGrantsManagerInternal ugm = mock(UriGrantsManagerInternal.class);
         when(ugm.checkGrantUriPermission(anyInt(), eq(null), any(Uri.class),
                 anyInt(), anyInt())).thenThrow(new SecurityException());
 
+        LocalServices.removeServiceForTest(UriGrantsManagerInternal.class);
+        LocalServices.addService(UriGrantsManagerInternal.class, ugm);
+
         Notification n = mock(Notification.class);
         when(n.getChannelId()).thenReturn(channel.getId());
         StatusBarNotification sbn =
                 new StatusBarNotification(PKG_O, PKG_O, id1, tag1, uid, uid, n, mUser, null, uid);
-        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
-        record.mAm = am;
 
-        record.calculateGrantableUris();
-        // should not throw
+        new NotificationRecord(mMockContext, sbn, channel); // should not throw
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index d5afe3b..0cdd9b8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -1880,6 +1880,11 @@
         final int dh = 2500;
         final int notchHeight = 200;
         setUpApp(new TestDisplayContent.Builder(mAtm, dw, dh).setNotch(notchHeight).build());
+        // The test assumes the notch will be at left side when the orientation is landscape.
+        if (mContext.getResources().getBoolean(
+                com.android.internal.R.bool.config_reverseDefaultRotation)) {
+            setReverseDefaultRotation(mActivity.mDisplayContent, false);
+        }
         addStatusBar(mActivity.mDisplayContent);
 
         mActivity.setVisible(false);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
index ae7b161..99688da 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java
@@ -48,6 +48,7 @@
 
 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
 
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
@@ -59,6 +60,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
 
 import android.annotation.IntDef;
@@ -956,6 +958,38 @@
         return testPlayer;
     }
 
+    /** Overrides the behavior of config_reverseDefaultRotation for the given display. */
+    void setReverseDefaultRotation(DisplayContent dc, boolean reverse) {
+        final DisplayRotation displayRotation = dc.getDisplayRotation();
+        if (!Mockito.mockingDetails(displayRotation).isSpy()) {
+            spyOn(displayRotation);
+        }
+        doAnswer(invocation -> {
+            invocation.callRealMethod();
+            final int w = invocation.getArgument(0);
+            final int h = invocation.getArgument(1);
+            if (w > h) {
+                if (reverse) {
+                    displayRotation.mPortraitRotation = Surface.ROTATION_90;
+                    displayRotation.mUpsideDownRotation = Surface.ROTATION_270;
+                } else {
+                    displayRotation.mPortraitRotation = Surface.ROTATION_270;
+                    displayRotation.mUpsideDownRotation = Surface.ROTATION_90;
+                }
+            } else {
+                if (reverse) {
+                    displayRotation.mLandscapeRotation = Surface.ROTATION_270;
+                    displayRotation.mSeascapeRotation = Surface.ROTATION_90;
+                } else {
+                    displayRotation.mLandscapeRotation = Surface.ROTATION_90;
+                    displayRotation.mSeascapeRotation = Surface.ROTATION_270;
+                }
+            }
+            return null;
+        }).when(displayRotation).configure(anyInt(), anyInt());
+        displayRotation.configure(dc.mBaseDisplayWidth, dc.mBaseDisplayHeight);
+    }
+
     /**
      * Avoids rotating screen disturbed by some conditions. It is usually used for the default
      * display that is not the instance of {@link TestDisplayContent} (it bypasses the conditions).
diff --git a/telecomm/java/android/telecom/Connection.java b/telecomm/java/android/telecom/Connection.java
index 943d8d6..4a541da 100644
--- a/telecomm/java/android/telecom/Connection.java
+++ b/telecomm/java/android/telecom/Connection.java
@@ -3388,7 +3388,11 @@
     public void onAbort() {}
 
     /**
-     * Notifies this Connection of a request to hold.
+     * Notifies this Connection of a request to hold. {@link Connection#setOnHold} should be within
+     * the onHold() body in order to transition the call state to {@link Connection#STATE_HOLDING}.
+     * <p>
+     * Note: If the Connection does not transition to  {@link Connection#STATE_HOLDING} within 2
+     * seconds, the call will be disconnected.
      */
     public void onHold() {}
 
diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt
index 31ea832..c92d768 100644
--- a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt
+++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapImage.kt
@@ -16,10 +16,6 @@
 
 package com.android.test.silkfx.hdr
 
-import android.animation.AnimatorSet
-import android.animation.ObjectAnimator
-import android.animation.ValueAnimator
-import android.animation.ValueAnimator.AnimatorUpdateListener
 import android.content.Context
 import android.graphics.Bitmap
 import android.graphics.Canvas
@@ -46,7 +42,7 @@
     private var selectedImage = -1
     private var outputMode = R.id.output_hdr
     private var bitmap: Bitmap? = null
-    private var gainmap: Gainmap? = null
+    private var originalGainmap: Gainmap? = null
     private var gainmapVisualizer: Bitmap? = null
     private lateinit var imageView: SubsamplingScaleImageView
     private lateinit var gainmapMetadataEditor: GainmapMetadataEditor
@@ -70,7 +66,6 @@
             it.check(outputMode)
             it.setOnCheckedChangeListener { _, checkedId ->
                 outputMode = checkedId
-                // Intentionally don't do anything fancy so that mode A/B comparisons are easy
                 updateDisplay()
             }
         }
@@ -101,41 +96,10 @@
 
         imageView.apply {
             isClickable = true
-            // Example of animating between SDR and HDR using gainmap params; animates HDR->SDR->HDR
-            // with a brief pause on SDR. The key thing here is that the gainmap's
-            // minDisplayRatioForHdrTransition is animated between its original value (for full HDR)
-            // and displayRatioForFullHdr (for full SDR). The view must also be invalidated during
-            // the animation for the updates to take effect.
             setOnClickListener {
-                if (gainmap != null && (outputMode == R.id.output_hdr ||
-                                        outputMode == R.id.output_hdr_test)) {
-                    val animationLengthMs: Long = 500
-                    val updateListener = object : AnimatorUpdateListener {
-                        override fun onAnimationUpdate(animation: ValueAnimator) {
-                            imageView.invalidate()
-                        }
-                    }
-                    val hdrToSdr = ObjectAnimator.ofFloat(
-                      gainmap, "minDisplayRatioForHdrTransition",
-                      gainmap!!.minDisplayRatioForHdrTransition,
-                      gainmap!!.displayRatioForFullHdr).apply {
-                        duration = animationLengthMs
-                        addUpdateListener(updateListener)
-                    }
-                    val sdrToHdr = ObjectAnimator.ofFloat(
-                      gainmap, "minDisplayRatioForHdrTransition",
-                      gainmap!!.displayRatioForFullHdr,
-                      gainmap!!.minDisplayRatioForHdrTransition).apply {
-                        duration = animationLengthMs
-                        addUpdateListener(updateListener)
-                    }
-
-                    AnimatorSet().apply {
-                        play(hdrToSdr)
-                        play(sdrToHdr).after(animationLengthMs)
-                        start()
-                    }
-                }
+                animate().alpha(.5f).withEndAction {
+                    animate().alpha(1f).start()
+                }.start()
             }
         }
     }
@@ -149,7 +113,7 @@
     }
 
     private fun doDecode(source: ImageDecoder.Source) {
-        gainmap = null
+        originalGainmap = null
         bitmap = ImageDecoder.decodeBitmap(source) { decoder, info, source ->
             decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
         }
@@ -167,9 +131,10 @@
             findViewById<TextView>(R.id.error_msg)!!.visibility = View.GONE
             findViewById<RadioGroup>(R.id.output_mode)!!.visibility = View.VISIBLE
 
-            gainmap = bitmap!!.gainmap
-            gainmapMetadataEditor.setGainmap(gainmap)
-            val map = gainmap!!.gainmapContents
+            val gainmap = bitmap!!.gainmap!!
+            originalGainmap = gainmap
+            gainmapMetadataEditor.setGainmap(Gainmap(gainmap, gainmap.gainmapContents))
+            val map = gainmap.gainmapContents
             if (map.config != Bitmap.Config.ALPHA_8) {
                 gainmapVisualizer = map
             } else {
@@ -198,14 +163,12 @@
 
         imageView.setImage(ImageSource.cachedBitmap(when (outputMode) {
             R.id.output_hdr -> {
-                gainmapMetadataEditor.useOriginalMetadata()
-                bitmap!!.gainmap = gainmap
+                bitmap!!.gainmap = originalGainmap
                 bitmap!!
             }
 
             R.id.output_hdr_test -> {
-                gainmapMetadataEditor.useEditMetadata()
-                bitmap!!.gainmap = gainmap
+                bitmap!!.gainmap = gainmapMetadataEditor.editedGainmap()
                 bitmap!!
             }
 
diff --git a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt
index 8a65304..c4bc600 100644
--- a/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt
+++ b/tests/SilkFX/src/com/android/test/silkfx/hdr/GainmapMetadataEditor.kt
@@ -28,23 +28,27 @@
 import com.android.test.silkfx.R
 
 data class GainmapMetadata(
-    var ratioMin: Float,
-    var ratioMax: Float,
-    var capacityMin: Float,
-    var capacityMax: Float,
-    var gamma: Float,
-    var offsetSdr: Float,
-    var offsetHdr: Float
+        var ratioMin: Float,
+        var ratioMax: Float,
+        var capacityMin: Float,
+        var capacityMax: Float,
+        var gamma: Float,
+        var offsetSdr: Float,
+        var offsetHdr: Float
 )
 
+/**
+ * Note: This can only handle single-channel gainmaps nicely. It will force all 3-channel
+ * metadata to have the same value single value and is not intended to be a robust demonstration
+ * of gainmap metadata editing
+ */
 class GainmapMetadataEditor(val parent: ViewGroup, val renderView: View) {
-    private var gainmap: Gainmap? = null
-    private var showingEdits = false
+    private lateinit var gainmap: Gainmap
 
     private var metadataPopup: PopupWindow? = null
 
     private var originalMetadata: GainmapMetadata = GainmapMetadata(
-        1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f)
+            1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 0.0f)
     private var currentMetadata: GainmapMetadata = originalMetadata.copy()
 
     private val maxProgress = 100.0f
@@ -61,23 +65,18 @@
     private val maxGamma = 3.0f
     // Min and max offsets are 0.0 and 1.0 respectively
 
-    fun setGainmap(newGainmap: Gainmap?) {
+    fun setGainmap(newGainmap: Gainmap) {
         gainmap = newGainmap
-        originalMetadata = GainmapMetadata(gainmap!!.getRatioMin()[0],
-            gainmap!!.getRatioMax()[0], gainmap!!.getMinDisplayRatioForHdrTransition(),
-            gainmap!!.getDisplayRatioForFullHdr(), gainmap!!.getGamma()[0],
-            gainmap!!.getEpsilonSdr()[0], gainmap!!.getEpsilonHdr()[0])
+        originalMetadata = GainmapMetadata(gainmap.getRatioMin()[0],
+                gainmap.getRatioMax()[0], gainmap.getMinDisplayRatioForHdrTransition(),
+                gainmap.getDisplayRatioForFullHdr(), gainmap.getGamma()[0],
+                gainmap.getEpsilonSdr()[0], gainmap.getEpsilonHdr()[0])
         currentMetadata = originalMetadata.copy()
     }
 
-    fun useOriginalMetadata() {
-        showingEdits = false
-        applyMetadata(originalMetadata)
-    }
-
-    fun useEditMetadata() {
-        showingEdits = true
+    fun editedGainmap(): Gainmap {
         applyMetadata(currentMetadata)
+        return gainmap
     }
 
     fun closeEditor() {
@@ -93,7 +92,7 @@
         val view = LayoutInflater.from(parent.getContext()).inflate(R.layout.gainmap_metadata, null)
 
         metadataPopup = PopupWindow(view, ViewGroup.LayoutParams.WRAP_CONTENT,
-            ViewGroup.LayoutParams.WRAP_CONTENT)
+                ViewGroup.LayoutParams.WRAP_CONTENT)
         metadataPopup!!.showAtLocation(view, Gravity.CENTER, 0, 0)
 
         (view.getParent() as ViewGroup).removeView(view)
@@ -117,7 +116,7 @@
         val offsetSdrSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_offsetsdr)
         val offsetHdrSeek = view.findViewById<SeekBar>(R.id.gainmap_metadata_offsethdr)
         arrayOf(gainmapMinSeek, gainmapMaxSeek, capacityMinSeek, capacityMaxSeek, gammaSeek,
-            offsetSdrSeek, offsetHdrSeek).forEach {
+                offsetSdrSeek, offsetHdrSeek).forEach {
             it.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener{
                 override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
                     if (!fromUser) return
@@ -149,37 +148,37 @@
         val offsetHdrSeek = parent.findViewById<SeekBar>(R.id.gainmap_metadata_offsethdr)
 
         gainmapMinSeek.setProgress(
-            ((currentMetadata.ratioMin - minRatioMin) / maxRatioMin * maxProgress).toInt())
+                ((currentMetadata.ratioMin - minRatioMin) / maxRatioMin * maxProgress).toInt())
         gainmapMaxSeek.setProgress(
-            ((currentMetadata.ratioMax - minRatioMax) / maxRatioMax * maxProgress).toInt())
+                ((currentMetadata.ratioMax - minRatioMax) / maxRatioMax * maxProgress).toInt())
         capacityMinSeek.setProgress(
-            ((currentMetadata.capacityMin - minCapacityMin) / maxCapacityMin * maxProgress).toInt())
+                ((currentMetadata.capacityMin - minCapacityMin) / maxCapacityMin * maxProgress).toInt())
         capacityMaxSeek.setProgress(
-            ((currentMetadata.capacityMax - minCapacityMax) / maxCapacityMax * maxProgress).toInt())
+                ((currentMetadata.capacityMax - minCapacityMax) / maxCapacityMax * maxProgress).toInt())
         gammaSeek.setProgress(
-            ((currentMetadata.gamma - minGamma) / maxGamma * maxProgress).toInt())
+                ((currentMetadata.gamma - minGamma) / maxGamma * maxProgress).toInt())
         // Log base 3 via: log_b(x) = log_y(x) / log_y(b)
         offsetSdrSeek.setProgress(
-            ((1.0 - Math.log(currentMetadata.offsetSdr.toDouble() / Math.log(3.0)) / -11.0)
-             .toFloat() * maxProgress).toInt())
+                ((1.0 - Math.log(currentMetadata.offsetSdr.toDouble() / Math.log(3.0)) / -11.0)
+                        .toFloat() * maxProgress).toInt())
         offsetHdrSeek.setProgress(
-            ((1.0 - Math.log(currentMetadata.offsetHdr.toDouble() / Math.log(3.0)) / -11.0)
-             .toFloat() * maxProgress).toInt())
+                ((1.0 - Math.log(currentMetadata.offsetHdr.toDouble() / Math.log(3.0)) / -11.0)
+                        .toFloat() * maxProgress).toInt())
 
         parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmin_val)!!.setText(
-            "%.3f".format(currentMetadata.ratioMin))
+                "%.3f".format(currentMetadata.ratioMin))
         parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmax_val)!!.setText(
-            "%.3f".format(currentMetadata.ratioMax))
+                "%.3f".format(currentMetadata.ratioMax))
         parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymin_val)!!.setText(
-            "%.3f".format(currentMetadata.capacityMin))
+                "%.3f".format(currentMetadata.capacityMin))
         parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymax_val)!!.setText(
-            "%.3f".format(currentMetadata.capacityMax))
+                "%.3f".format(currentMetadata.capacityMax))
         parent.findViewById<TextView>(R.id.gainmap_metadata_gamma_val)!!.setText(
-            "%.3f".format(currentMetadata.gamma))
+                "%.3f".format(currentMetadata.gamma))
         parent.findViewById<TextView>(R.id.gainmap_metadata_offsetsdr_val)!!.setText(
-            "%.5f".format(currentMetadata.offsetSdr))
+                "%.5f".format(currentMetadata.offsetSdr))
         parent.findViewById<TextView>(R.id.gainmap_metadata_offsethdr_val)!!.setText(
-            "%.5f".format(currentMetadata.offsetHdr))
+                "%.5f".format(currentMetadata.offsetHdr))
     }
 
     private fun resetGainmapMetadata() {
@@ -189,69 +188,59 @@
     }
 
     private fun applyMetadata(newMetadata: GainmapMetadata) {
-        gainmap!!.setRatioMin(newMetadata.ratioMin, newMetadata.ratioMin, newMetadata.ratioMin)
-        gainmap!!.setRatioMax(newMetadata.ratioMax, newMetadata.ratioMax, newMetadata.ratioMax)
-        gainmap!!.setMinDisplayRatioForHdrTransition(newMetadata.capacityMin)
-        gainmap!!.setDisplayRatioForFullHdr(newMetadata.capacityMax)
-        gainmap!!.setGamma(newMetadata.gamma, newMetadata.gamma, newMetadata.gamma)
-        gainmap!!.setEpsilonSdr(newMetadata.offsetSdr, newMetadata.offsetSdr, newMetadata.offsetSdr)
-        gainmap!!.setEpsilonHdr(newMetadata.offsetHdr, newMetadata.offsetHdr, newMetadata.offsetHdr)
+        gainmap.setRatioMin(newMetadata.ratioMin, newMetadata.ratioMin, newMetadata.ratioMin)
+        gainmap.setRatioMax(newMetadata.ratioMax, newMetadata.ratioMax, newMetadata.ratioMax)
+        gainmap.setMinDisplayRatioForHdrTransition(newMetadata.capacityMin)
+        gainmap.setDisplayRatioForFullHdr(newMetadata.capacityMax)
+        gainmap.setGamma(newMetadata.gamma, newMetadata.gamma, newMetadata.gamma)
+        gainmap.setEpsilonSdr(newMetadata.offsetSdr, newMetadata.offsetSdr, newMetadata.offsetSdr)
+        gainmap.setEpsilonHdr(newMetadata.offsetHdr, newMetadata.offsetHdr, newMetadata.offsetHdr)
         renderView.invalidate()
     }
 
     private fun updateGainmapMin(normalized: Float) {
         val newValue = minRatioMin + normalized * (maxRatioMin - minRatioMin)
         parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmin_val)!!.setText(
-            "%.3f".format(newValue))
+                "%.3f".format(newValue))
         currentMetadata.ratioMin = newValue
-        if (showingEdits) {
-            gainmap!!.setRatioMin(newValue, newValue, newValue)
-            renderView.invalidate()
-        }
+        gainmap.setRatioMin(newValue, newValue, newValue)
+        renderView.invalidate()
     }
 
     private fun updateGainmapMax(normalized: Float) {
         val newValue = minRatioMax + normalized * (maxRatioMax - minRatioMax)
         parent.findViewById<TextView>(R.id.gainmap_metadata_gainmapmax_val)!!.setText(
-            "%.3f".format(newValue))
+                "%.3f".format(newValue))
         currentMetadata.ratioMax = newValue
-        if (showingEdits) {
-            gainmap!!.setRatioMax(newValue, newValue, newValue)
-            renderView.invalidate()
-        }
+        gainmap.setRatioMax(newValue, newValue, newValue)
+        renderView.invalidate()
     }
 
     private fun updateCapacityMin(normalized: Float) {
         val newValue = minCapacityMin + normalized * (maxCapacityMin - minCapacityMin)
         parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymin_val)!!.setText(
-            "%.3f".format(newValue))
+                "%.3f".format(newValue))
         currentMetadata.capacityMin = newValue
-        if (showingEdits) {
-            gainmap!!.setMinDisplayRatioForHdrTransition(newValue)
-            renderView.invalidate()
-        }
+        gainmap.setMinDisplayRatioForHdrTransition(newValue)
+        renderView.invalidate()
     }
 
     private fun updateCapacityMax(normalized: Float) {
         val newValue = minCapacityMax + normalized * (maxCapacityMax - minCapacityMax)
         parent.findViewById<TextView>(R.id.gainmap_metadata_capacitymax_val)!!.setText(
-            "%.3f".format(newValue))
+                "%.3f".format(newValue))
         currentMetadata.capacityMax = newValue
-        if (showingEdits) {
-            gainmap!!.setDisplayRatioForFullHdr(newValue)
-            renderView.invalidate()
-        }
+        gainmap.setDisplayRatioForFullHdr(newValue)
+        renderView.invalidate()
     }
 
     private fun updateGamma(normalized: Float) {
         val newValue = minGamma + normalized * (maxGamma - minGamma)
         parent.findViewById<TextView>(R.id.gainmap_metadata_gamma_val)!!.setText(
-            "%.3f".format(newValue))
+                "%.3f".format(newValue))
         currentMetadata.gamma = newValue
-        if (showingEdits) {
-            gainmap!!.setGamma(newValue, newValue, newValue)
-            renderView.invalidate()
-        }
+        gainmap.setGamma(newValue, newValue, newValue)
+        renderView.invalidate()
     }
 
     private fun updateOffsetSdr(normalized: Float) {
@@ -260,12 +249,10 @@
             newValue = Math.pow(3.0, (1.0 - normalized.toDouble()) * -11.0).toFloat()
         }
         parent.findViewById<TextView>(R.id.gainmap_metadata_offsetsdr_val)!!.setText(
-            "%.5f".format(newValue))
+                "%.5f".format(newValue))
         currentMetadata.offsetSdr = newValue
-        if (showingEdits) {
-            gainmap!!.setEpsilonSdr(newValue, newValue, newValue)
-            renderView.invalidate()
-        }
+        gainmap.setEpsilonSdr(newValue, newValue, newValue)
+        renderView.invalidate()
     }
 
     private fun updateOffsetHdr(normalized: Float) {
@@ -274,11 +261,9 @@
             newValue = Math.pow(3.0, (1.0 - normalized.toDouble()) * -11.0).toFloat()
         }
         parent.findViewById<TextView>(R.id.gainmap_metadata_offsethdr_val)!!.setText(
-            "%.5f".format(newValue))
+                "%.5f".format(newValue))
         currentMetadata.offsetHdr = newValue
-        if (showingEdits) {
-            gainmap!!.setEpsilonHdr(newValue, newValue, newValue)
-            renderView.invalidate()
-        }
+        gainmap.setEpsilonHdr(newValue, newValue, newValue)
+        renderView.invalidate()
     }
 }