Merge "Move CommunalSwipeDetector to SystemUI/src/.../communal/" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 9298ca2..73b35b2 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -61775,7 +61775,7 @@
   public final class BackEvent {
     ctor public BackEvent(float, float, float, int);
     ctor @FlaggedApi("com.android.window.flags.predictive_back_timestamp_api") public BackEvent(float, float, float, int, long);
-    method @FlaggedApi("com.android.window.flags.predictive_back_timestamp_api") public long getFrameTime();
+    method @FlaggedApi("com.android.window.flags.predictive_back_timestamp_api") public long getFrameTimeMillis();
     method @FloatRange(from=0, to=1) public float getProgress();
     method public int getSwipeEdge();
     method public float getTouchX();
diff --git a/core/java/android/window/BackEvent.java b/core/java/android/window/BackEvent.java
index 90fac361..1b9235b 100644
--- a/core/java/android/window/BackEvent.java
+++ b/core/java/android/window/BackEvent.java
@@ -58,7 +58,7 @@
     private final float mTouchX;
     private final float mTouchY;
     private final float mProgress;
-    private final long mFrameTime;
+    private final long mFrameTimeMillis;
 
     @SwipeEdge
     private final int mSwipeEdge;
@@ -68,7 +68,7 @@
         if (predictiveBackTimestampApi()) {
             return new BackEvent(backMotionEvent.getTouchX(), backMotionEvent.getTouchY(),
                     backMotionEvent.getProgress(), backMotionEvent.getSwipeEdge(),
-                    backMotionEvent.getFrameTime());
+                    backMotionEvent.getFrameTimeMillis());
         } else {
             return new BackEvent(backMotionEvent.getTouchX(), backMotionEvent.getTouchY(),
                     backMotionEvent.getProgress(), backMotionEvent.getSwipeEdge());
@@ -88,7 +88,7 @@
         mTouchY = touchY;
         mProgress = progress;
         mSwipeEdge = swipeEdge;
-        mFrameTime = System.nanoTime() / TimeUtils.NANOS_PER_MS;
+        mFrameTimeMillis = System.nanoTime() / TimeUtils.NANOS_PER_MS;
     }
 
     /**
@@ -98,16 +98,16 @@
      * @param touchY Absolute Y location of the touch point of this event.
      * @param progress Value between 0 and 1 on how far along the back gesture is.
      * @param swipeEdge Indicates which edge the swipe starts from.
-     * @param frameTime frame time of the back event.
+     * @param frameTimeMillis frame time of the back event.
      */
     @FlaggedApi(FLAG_PREDICTIVE_BACK_TIMESTAMP_API)
     public BackEvent(float touchX, float touchY, float progress, @SwipeEdge int swipeEdge,
-            long frameTime) {
+            long frameTimeMillis) {
         mTouchX = touchX;
         mTouchY = touchY;
         mProgress = progress;
         mSwipeEdge = swipeEdge;
-        mFrameTime = frameTime;
+        mFrameTimeMillis = frameTimeMillis;
     }
 
     /**
@@ -160,8 +160,8 @@
      * Returns the frameTime of the BackEvent in milliseconds. Useful for calculating velocity.
      */
     @FlaggedApi(FLAG_PREDICTIVE_BACK_TIMESTAMP_API)
-    public long getFrameTime() {
-        return mFrameTime;
+    public long getFrameTimeMillis() {
+        return mFrameTimeMillis;
     }
 
     @Override
@@ -177,7 +177,7 @@
                 && mTouchY == that.mTouchY
                 && mProgress == that.mProgress
                 && mSwipeEdge == that.mSwipeEdge
-                && mFrameTime == that.mFrameTime;
+                && mFrameTimeMillis == that.mFrameTimeMillis;
     }
 
     @Override
@@ -187,7 +187,7 @@
                 + ", mTouchY=" + mTouchY
                 + ", mProgress=" + mProgress
                 + ", mSwipeEdge=" + mSwipeEdge
-                + ", mFrameTime=" + mFrameTime + "ms"
+                + ", mFrameTimeMillis=" + mFrameTimeMillis
                 + "}";
     }
 }
diff --git a/core/java/android/window/BackMotionEvent.java b/core/java/android/window/BackMotionEvent.java
index a8ec4ee..cc2afbc 100644
--- a/core/java/android/window/BackMotionEvent.java
+++ b/core/java/android/window/BackMotionEvent.java
@@ -33,7 +33,7 @@
 public final class BackMotionEvent implements Parcelable {
     private final float mTouchX;
     private final float mTouchY;
-    private final long mFrameTime;
+    private final long mFrameTimeMillis;
     private final float mProgress;
     private final boolean mTriggerBack;
 
@@ -49,7 +49,7 @@
      *
      * @param touchX Absolute X location of the touch point of this event.
      * @param touchY Absolute Y location of the touch point of this event.
-     * @param frameTime Event time of the corresponding touch event.
+     * @param frameTimeMillis Event time of the corresponding touch event.
      * @param progress Value between 0 and 1 on how far along the back gesture is.
      * @param triggerBack Indicates whether the back arrow is in the triggered state or not
      * @param swipeEdge Indicates which edge the swipe starts from.
@@ -59,14 +59,14 @@
     public BackMotionEvent(
             float touchX,
             float touchY,
-            long frameTime,
+            long frameTimeMillis,
             float progress,
             boolean triggerBack,
             @BackEvent.SwipeEdge int swipeEdge,
             @Nullable RemoteAnimationTarget departingAnimationTarget) {
         mTouchX = touchX;
         mTouchY = touchY;
-        mFrameTime = frameTime;
+        mFrameTimeMillis = frameTimeMillis;
         mProgress = progress;
         mTriggerBack = triggerBack;
         mSwipeEdge = swipeEdge;
@@ -80,7 +80,7 @@
         mTriggerBack = in.readBoolean();
         mSwipeEdge = in.readInt();
         mDepartingAnimationTarget = in.readTypedObject(RemoteAnimationTarget.CREATOR);
-        mFrameTime = in.readLong();
+        mFrameTimeMillis = in.readLong();
     }
 
     @NonNull
@@ -109,7 +109,7 @@
         dest.writeBoolean(mTriggerBack);
         dest.writeInt(mSwipeEdge);
         dest.writeTypedObject(mDepartingAnimationTarget, flags);
-        dest.writeLong(mFrameTime);
+        dest.writeLong(mFrameTimeMillis);
     }
 
     /**
@@ -156,8 +156,8 @@
     /**
      * Returns the frame time of the BackMotionEvent in milliseconds.
      */
-    public long getFrameTime() {
-        return mFrameTime;
+    public long getFrameTimeMillis() {
+        return mFrameTimeMillis;
     }
 
     /**
@@ -175,7 +175,7 @@
         return "BackMotionEvent{"
                 + "mTouchX=" + mTouchX
                 + ", mTouchY=" + mTouchY
-                + ", mFrameTime=" + mFrameTime + "ms"
+                + ", mFrameTimeMillis=" + mFrameTimeMillis
                 + ", mProgress=" + mProgress
                 + ", mTriggerBack=" + mTriggerBack
                 + ", mSwipeEdge=" + mSwipeEdge
diff --git a/core/java/android/window/BackTouchTracker.java b/core/java/android/window/BackTouchTracker.java
index 39a3025..4908068 100644
--- a/core/java/android/window/BackTouchTracker.java
+++ b/core/java/android/window/BackTouchTracker.java
@@ -151,7 +151,7 @@
         return new BackMotionEvent(
                 /* touchX = */ mInitTouchX,
                 /* touchY = */ mInitTouchY,
-                /* frameTime = */ 0,
+                /* frameTimeMillis = */ 0,
                 /* progress = */ 0,
                 /* triggerBack = */ mTriggerBack,
                 /* swipeEdge = */ mSwipeEdge,
@@ -236,7 +236,7 @@
         return new BackMotionEvent(
                 /* touchX = */ mLatestTouchX,
                 /* touchY = */ mLatestTouchY,
-                /* frameTime = */ 0,
+                /* frameTimeMillis = */ 0,
                 /* progress = */ progress,
                 /* triggerBack = */ mTriggerBack,
                 /* swipeEdge = */ mSwipeEdge,
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index 8db1f95..bd01899 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -238,7 +238,7 @@
             try {
                 long frameTime = 0;
                 if (predictiveBackTimestampApi()) {
-                    frameTime = backEvent.getFrameTime();
+                    frameTime = backEvent.getFrameTimeMillis();
                 }
                 mIOnBackInvokedCallback.onBackStarted(
                         new BackMotionEvent(backEvent.getTouchX(), backEvent.getTouchY(), frameTime,
@@ -254,7 +254,7 @@
             try {
                 long frameTime = 0;
                 if (predictiveBackTimestampApi()) {
-                    frameTime = backEvent.getFrameTime();
+                    frameTime = backEvent.getFrameTimeMillis();
                 }
                 mIOnBackInvokedCallback.onBackProgressed(
                         new BackMotionEvent(backEvent.getTouchX(), backEvent.getTouchY(), frameTime,
diff --git a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
index 46bd73e..4d6c30e 100644
--- a/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
+++ b/core/tests/coretests/src/android/window/WindowOnBackInvokedDispatcherTest.java
@@ -686,7 +686,7 @@
         return new BackMotionEvent(
                 /* touchX = */ 0,
                 /* touchY = */ 0,
-                /* frameTime = */ 0,
+                /* frameTimeMillis = */ 0,
                 /* progress = */ progress,
                 /* triggerBack = */ false,
                 /* swipeEdge = */ BackEvent.EDGE_LEFT,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
index 52391d2..dc50fdb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossTaskBackAnimation.java
@@ -333,7 +333,7 @@
     private void onGestureProgress(@NonNull BackEvent backEvent) {
         if (!mBackInProgress) {
             mBackInProgress = true;
-            mDownTime = backEvent.getFrameTime();
+            mDownTime = backEvent.getFrameTimeMillis();
         }
         float progress = backEvent.getProgress();
         mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
@@ -342,7 +342,7 @@
             mVelocityTracker.addMovement(
                     MotionEvent.obtain(
                             /* downTime */ mDownTime,
-                            /* eventTime */ backEvent.getFrameTime(),
+                            /* eventTime */ backEvent.getFrameTimeMillis(),
                             /* action */ ACTION_MOVE,
                             /* x */ interpolatedProgress * SPRING_SCALE,
                             /* y */ 0f,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java
index 915a8a1..37369d1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIStatusManager.java
@@ -24,6 +24,7 @@
 /** Handle the visibility state of the Compat UI components. */
 public class CompatUIStatusManager {
 
+    private static final int COMPAT_UI_EDUCATION_UNDEFINED = -1;
     public static final int COMPAT_UI_EDUCATION_HIDDEN = 0;
     public static final int COMPAT_UI_EDUCATION_VISIBLE = 1;
 
@@ -32,24 +33,40 @@
     @NonNull
     private final IntSupplier mReader;
 
+    private int mCurrentValue = COMPAT_UI_EDUCATION_UNDEFINED;
+
     public CompatUIStatusManager(@NonNull IntConsumer writer, @NonNull IntSupplier reader) {
         mWriter = writer;
         mReader = reader;
     }
 
     public CompatUIStatusManager() {
-        this(i -> { }, () -> COMPAT_UI_EDUCATION_HIDDEN);
+        this(i -> {
+        }, () -> COMPAT_UI_EDUCATION_HIDDEN);
     }
 
     void onEducationShown() {
-        mWriter.accept(COMPAT_UI_EDUCATION_VISIBLE);
+        if (mCurrentValue != COMPAT_UI_EDUCATION_VISIBLE) {
+            mCurrentValue = COMPAT_UI_EDUCATION_VISIBLE;
+            mWriter.accept(mCurrentValue);
+        }
     }
 
     void onEducationHidden() {
-        mWriter.accept(COMPAT_UI_EDUCATION_HIDDEN);
+        if (mCurrentValue != COMPAT_UI_EDUCATION_HIDDEN) {
+            mCurrentValue = COMPAT_UI_EDUCATION_HIDDEN;
+            mWriter.accept(mCurrentValue);
+        }
     }
 
     boolean isEducationVisible() {
-        return mReader.getAsInt() == COMPAT_UI_EDUCATION_VISIBLE;
+        return getCurrentValue() == COMPAT_UI_EDUCATION_VISIBLE;
     }
-}
+
+    private int getCurrentValue() {
+        if (mCurrentValue == COMPAT_UI_EDUCATION_UNDEFINED) {
+            mCurrentValue = mReader.getAsInt();
+        }
+        return mCurrentValue;
+    }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 346f21b..7c9cd08 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -18,6 +18,7 @@
 
 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER;
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
@@ -58,6 +59,7 @@
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.os.SystemProperties;
+import android.os.Trace;
 import android.provider.Settings;
 import android.util.ArrayMap;
 import android.util.Log;
@@ -800,8 +802,17 @@
         track.mReadyTransitions.add(active);
 
         for (int i = 0; i < mObservers.size(); ++i) {
+            final boolean useTrace = Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER);
+            if (useTrace) {
+                Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
+                        mObservers.get(i).getClass().getSimpleName() + "#onTransitionReady: "
+                                + transitTypeToString(info.getType()));
+            }
             mObservers.get(i).onTransitionReady(
                     active.mToken, info, active.mStartT, active.mFinishT);
+            if (useTrace) {
+                Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+            }
         }
 
         /*
@@ -931,7 +942,7 @@
                 onFinish(ready.mToken, null);
                 return;
             }
-            playTransition(ready);
+            playTransitionWithTracing(ready);
             // Attempt to merge any more queued-up transitions.
             processReadyQueue(track);
             return;
@@ -1003,6 +1014,18 @@
         processReadyQueue(track);
     }
 
+    private void playTransitionWithTracing(@NonNull ActiveTransition active) {
+        final boolean useTrace = Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER);
+        if (useTrace) {
+            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
+                    "playTransition: " + transitTypeToString(active.mInfo.getType()));
+        }
+        playTransition(active);
+        if (useTrace) {
+            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+        }
+    }
+
     private void playTransition(@NonNull ActiveTransition active) {
         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Playing animation for %s", active);
         final var token = active.mToken;
@@ -1022,6 +1045,12 @@
             if (consumed) {
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by firstHandler");
                 mTransitionTracer.logDispatched(active.mInfo.getDebugId(), active.mHandler);
+                if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+                    Trace.instant(TRACE_TAG_WINDOW_MANAGER,
+                            active.mHandler.getClass().getSimpleName()
+                                    + "#startAnimation animated "
+                                    + transitTypeToString(active.mInfo.getType()));
+                }
                 return;
             }
         }
@@ -1052,6 +1081,12 @@
                 ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " animated by %s",
                         mHandlers.get(i));
                 mTransitionTracer.logDispatched(info.getDebugId(), mHandlers.get(i));
+                if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
+                    Trace.instant(TRACE_TAG_WINDOW_MANAGER,
+                            mHandlers.get(i).getClass().getSimpleName()
+                                    + "#startAnimation animated "
+                                    + transitTypeToString(info.getType()));
+                }
                 return mHandlers.get(i);
             }
         }
@@ -1059,6 +1094,26 @@
                 "This shouldn't happen, maybe the default handler is broken.");
     }
 
+    private Pair<TransitionHandler, WindowContainerTransaction> dispatchRequestWithTracing(
+            @NonNull IBinder transition, @NonNull TransitionRequestInfo request,
+            @Nullable TransitionHandler skip) {
+        final boolean useTrace = Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER);
+        if (useTrace) {
+            Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER,
+                    "dispatchRequest: " + transitTypeToString(request.getType()));
+        }
+        Pair<TransitionHandler, WindowContainerTransaction> result =
+                dispatchRequest(transition, request, skip);
+        if (useTrace) {
+            if (result != null) {
+                Trace.instant(TRACE_TAG_WINDOW_MANAGER, result.first.getClass().getSimpleName()
+                        + "#handleRequest handled " + transitTypeToString(request.getType()));
+            }
+            Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
+        }
+        return result;
+    }
+
     /**
      * Gives every handler (in order) a chance to handle request until one consumes the transition.
      * @return the WindowContainerTransaction given by the handler which consumed the transition.
@@ -1197,12 +1252,11 @@
             mSleepHandler.handleRequest(transitionToken, request);
             active.mHandler = mSleepHandler;
         } else {
-            for (int i = mHandlers.size() - 1; i >= 0; --i) {
-                wct = mHandlers.get(i).handleRequest(transitionToken, request);
-                if (wct != null) {
-                    active.mHandler = mHandlers.get(i);
-                    break;
-                }
+            Pair<TransitionHandler, WindowContainerTransaction> requestResult =
+                    dispatchRequestWithTracing(transitionToken, request, /* skip= */ null);
+            if (requestResult != null) {
+                active.mHandler = requestResult.first;
+                wct = requestResult.second;
             }
             if (request.getDisplayChange() != null) {
                 TransitionRequestInfo.DisplayChange change = request.getDisplayChange();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
index 60c9222..78e7962 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtility.java
@@ -29,8 +29,6 @@
 import android.view.SurfaceControl;
 import android.window.DesktopModeFlags;
 
-import androidx.annotation.NonNull;
-
 import com.android.wm.shell.R;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus;
@@ -129,13 +127,15 @@
 
         // If width or height are negative or exceeding the width or height constraints, revert the
         // respective bounds to use previous bound dimensions.
-        if (isExceedingWidthConstraint(repositionTaskBounds, stableBounds, displayController,
+        if (isExceedingWidthConstraint(repositionTaskBounds.width(),
+                /* startingWidth= */ oldRight - oldLeft, stableBounds, displayController,
                 windowDecoration)) {
             repositionTaskBounds.right = oldRight;
             repositionTaskBounds.left = oldLeft;
             isAspectRatioMaintained = false;
         }
-        if (isExceedingHeightConstraint(repositionTaskBounds, stableBounds, displayController,
+        if (isExceedingHeightConstraint(repositionTaskBounds.height(),
+                /* startingHeight= */oldBottom - oldTop, stableBounds, displayController,
                 windowDecoration)) {
             repositionTaskBounds.top = oldTop;
             repositionTaskBounds.bottom = oldBottom;
@@ -208,28 +208,34 @@
         return result;
     }
 
-    private static boolean isExceedingWidthConstraint(@NonNull Rect repositionTaskBounds,
+    private static boolean isExceedingWidthConstraint(int repositionedWidth, int startingWidth,
             Rect maxResizeBounds, DisplayController displayController,
             WindowDecoration windowDecoration) {
+        boolean isSizeIncreasing = (repositionedWidth - startingWidth) > 0;
         // Check if width is less than the minimum width constraint.
-        if (repositionTaskBounds.width() < getMinWidth(displayController, windowDecoration)) {
-            return true;
+        if (repositionedWidth < getMinWidth(displayController, windowDecoration)) {
+            // Only allow width to be increased if it is already below minimum.
+            return !isSizeIncreasing;
         }
         // Check if width is more than the maximum resize bounds on desktop windowing mode.
+        // Only allow width to be decreased if it already exceeds maximum.
         return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
-                && repositionTaskBounds.width() > maxResizeBounds.width();
+                && repositionedWidth > maxResizeBounds.width() && isSizeIncreasing;
     }
 
-    private static boolean isExceedingHeightConstraint(@NonNull Rect repositionTaskBounds,
+    private static boolean isExceedingHeightConstraint(int repositionedHeight, int startingHeight,
             Rect maxResizeBounds, DisplayController displayController,
             WindowDecoration windowDecoration) {
+        boolean isSizeIncreasing = (repositionedHeight - startingHeight) > 0;
         // Check if height is less than the minimum height constraint.
-        if (repositionTaskBounds.height() < getMinHeight(displayController, windowDecoration)) {
-            return true;
+        if (repositionedHeight < getMinHeight(displayController, windowDecoration)) {
+            // Only allow height to be increased if it is already below minimum.
+            return !isSizeIncreasing;
         }
         // Check if height is more than the maximum resize bounds on desktop windowing mode.
+        // Only allow height to be decreased if it already exceeds maximum.
         return isSizeConstraintForDesktopModeEnabled(windowDecoration.mDecorWindowContext)
-                && repositionTaskBounds.height() > maxResizeBounds.height();
+                && repositionedHeight > maxResizeBounds.height() && isSizeIncreasing;
     }
 
     private static float getMinWidth(DisplayController displayController,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java
index d6059a8..8fd7c0e 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java
@@ -16,6 +16,10 @@
 
 package com.android.wm.shell.compatui;
 
+import static com.android.wm.shell.compatui.CompatUIStatusManager.COMPAT_UI_EDUCATION_HIDDEN;
+
+import static junit.framework.Assert.assertEquals;
+
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
@@ -25,7 +29,6 @@
 
 import com.android.wm.shell.ShellTestCase;
 
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -63,13 +66,75 @@
         assertFalse(mStatusManager.isEducationVisible());
     }
 
+    @Test
+    public void valuesAreCached() {
+        // At the beginning the value is not read or written because
+        // we access the reader in lazy way.
+        mTestState.assertReaderInvocations(0);
+        mTestState.assertWriterInvocations(0);
+
+        // We read the value when we start. Initial value is hidden.
+        assertFalse(mStatusManager.isEducationVisible());
+        mTestState.assertReaderInvocations(1);
+        mTestState.assertWriterInvocations(0);
+
+        // We send the event for the same state which is not written.
+        mStatusManager.onEducationHidden();
+        assertFalse(mStatusManager.isEducationVisible());
+        mTestState.assertReaderInvocations(1);
+        mTestState.assertWriterInvocations(0);
+
+        // We send the event for the different state which is written but
+        // not read again.
+        mStatusManager.onEducationShown();
+        assertTrue(mStatusManager.isEducationVisible());
+        mTestState.assertReaderInvocations(1);
+        mTestState.assertWriterInvocations(1);
+
+        // We read multiple times and we don't read the value again
+        mStatusManager.isEducationVisible();
+        mStatusManager.isEducationVisible();
+        mStatusManager.isEducationVisible();
+        mTestState.assertReaderInvocations(1);
+        mTestState.assertWriterInvocations(1);
+
+        // We write different values. Writer  is only accessed when
+        // the value changes.
+        mStatusManager.onEducationHidden(); // change
+        mStatusManager.onEducationHidden();
+        mStatusManager.onEducationShown(); // change
+        mStatusManager.onEducationShown();
+        mStatusManager.onEducationHidden(); // change
+        mStatusManager.onEducationShown(); // change
+        mTestState.assertReaderInvocations(1);
+        mTestState.assertWriterInvocations(5);
+    }
+
     static class FakeCompatUIStatusManagerTest {
 
-        int mCurrentStatus = 0;
+        int mCurrentStatus = COMPAT_UI_EDUCATION_HIDDEN;
 
-        final IntSupplier mReader = () -> mCurrentStatus;
+        int mReaderInvocations;
 
-        final IntConsumer mWriter = newStatus -> mCurrentStatus = newStatus;
+        int mWriterInvocations;
+
+        final IntSupplier mReader = () -> {
+            mReaderInvocations++;
+            return mCurrentStatus;
+        };
+
+        final IntConsumer mWriter = newStatus -> {
+            mWriterInvocations++;
+            mCurrentStatus = newStatus;
+        };
+
+        void assertWriterInvocations(int expected) {
+            assertEquals(expected, mWriterInvocations);
+        }
+
+        void assertReaderInvocations(int expected) {
+            assertEquals(expected, mReaderInvocations);
+        }
 
     }
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
index 24f6bec..a20a89c 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt
@@ -36,6 +36,7 @@
 import com.android.wm.shell.common.DisplayLayout
 import com.android.wm.shell.shared.desktopmode.DesktopModeStatus
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM
+import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT
 import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP
 import com.google.common.truth.Truth.assertThat
@@ -48,9 +49,9 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.any
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 import org.mockito.quality.Strictness
+import org.mockito.Mockito.`when` as whenever
 
 /**
  * Tests for [DragPositioningCallbackUtility].
@@ -193,6 +194,62 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+    fun testChangeBounds_unresizeableApp_initialHeightLessThanMin_increasingBounds_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(BELOW_MIN_HEIGHT_BOUNDS.right.toFloat(),
+            BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(BELOW_MIN_HEIGHT_BOUNDS)
+
+        // Resize to increased bounds
+        val newX = BELOW_MIN_HEIGHT_BOUNDS.right.toFloat() + 20
+        val newY = BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat() + 10
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is in direction of desired range
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+                repositionTaskBounds, BELOW_MIN_HEIGHT_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.right + 20)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.bottom + 10)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+    fun testChangeBounds_unresizeableApp_initialHeightMoreThanMax_decreasingBounds_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat(),
+            EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat())
+        val repositionTaskBounds = Rect(EXCEEDS_MAX_HEIGHT_BOUNDS)
+
+        // Resize to decreased bounds.
+        val newX = EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat() - 10
+        val newY = EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat() + 20
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is in direction of desired range.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+                repositionTaskBounds, EXCEEDS_MAX_HEIGHT_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.top + 20)
+        assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.right - 10)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.bottom)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
     fun testChangeBounds_unresizeableApp_widthLessThanMin_resetToStartingBounds() {
         mockWindowDecoration.mTaskInfo.isResizeable = false
         val startingPoint = PointF(STARTING_BOUNDS.right.toFloat(), STARTING_BOUNDS.top.toFloat())
@@ -211,13 +268,68 @@
             )
         )
 
-
         assertThat(repositionTaskBounds.left).isEqualTo(STARTING_BOUNDS.left)
         assertThat(repositionTaskBounds.top).isEqualTo(STARTING_BOUNDS.top)
         assertThat(repositionTaskBounds.right).isEqualTo(STARTING_BOUNDS.right)
         assertThat(repositionTaskBounds.bottom).isEqualTo(STARTING_BOUNDS.bottom)
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+    fun testChangeBounds_unresizeableApp_initialWidthLessThanMin_increasingBounds_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(BELOW_MIN_WIDTH_BOUNDS.right.toFloat(),
+            BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(BELOW_MIN_WIDTH_BOUNDS)
+
+        // Resize to increased bounds.
+        val newX = BELOW_MIN_WIDTH_BOUNDS.right.toFloat() + 10
+        val newY = BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat() + 20
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is in direction of desired range.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+                repositionTaskBounds, BELOW_MIN_WIDTH_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.right + 10)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.bottom + 20)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_SCALED_RESIZING)
+    fun testChangeBounds_unresizeableApp_initialWidthMoreThanMax_decreasingBounds_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat(),
+            EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat())
+        val repositionTaskBounds = Rect(EXCEEDS_MAX_WIDTH_BOUNDS)
+
+        // Resize to decreased bounds.
+        val newX = EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat() + 20
+        val newY = EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat() + 10
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is in direction of desired range.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_LEFT or CTRL_TYPE_TOP,
+                repositionTaskBounds, EXCEEDS_MAX_WIDTH_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.left + 20)
+        assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.top + 10)
+        assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.right)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.bottom)
+    }
+
 
     @Test
     fun testChangeBoundsDoesNotChangeHeightWhenNegative() {
@@ -427,6 +539,60 @@
 
     @Test
     @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testMinHeight_initialHeightLessThanMin_increasingHeight_resizeAllowed() {
+        val startingPoint = PointF(BELOW_MIN_HEIGHT_BOUNDS.right.toFloat(),
+            BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(BELOW_MIN_HEIGHT_BOUNDS)
+
+        // Attempt to increase height.
+        val newX = BELOW_MIN_HEIGHT_BOUNDS.right.toFloat()
+        val newY = BELOW_MIN_HEIGHT_BOUNDS.bottom.toFloat() + 10
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is increasing height closer to valid region.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+                repositionTaskBounds, BELOW_MIN_HEIGHT_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.right)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_HEIGHT_BOUNDS.bottom + 10)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testMinWidth_initialWidthLessThanMin_increasingBounds_resizeAllowed() {
+        val startingPoint = PointF(BELOW_MIN_WIDTH_BOUNDS.right.toFloat(),
+            BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat())
+        val repositionTaskBounds = Rect(BELOW_MIN_WIDTH_BOUNDS)
+
+        // Attempt to increase width.
+        val newX = BELOW_MIN_WIDTH_BOUNDS.right.toFloat() + 10
+        val newY = BELOW_MIN_WIDTH_BOUNDS.bottom.toFloat()
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is increasing width closer to valid region.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_BOTTOM,
+                repositionTaskBounds, BELOW_MIN_WIDTH_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.right + 10)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(BELOW_MIN_WIDTH_BOUNDS.bottom)
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
     fun taskMinWidthHeightUndefined_changeBoundsInDesktopModeAllowedSize_shouldChangeBounds() {
         doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(mockContext) }
         initializeTaskInfo(taskMinWidth = -1, taskMinHeight = -1)
@@ -547,6 +713,61 @@
         assertThat(repositionTaskBounds.height()).isLessThan(STABLE_BOUNDS.bottom)
     }
 
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testMaxHeight_initialHeightMoreThanMax_decreasingHeight_resizeAllowed() {
+        mockWindowDecoration.mTaskInfo.isResizeable = false
+        val startingPoint = PointF(EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat(),
+            EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat())
+        val repositionTaskBounds = Rect(EXCEEDS_MAX_HEIGHT_BOUNDS)
+
+        // Attempt to decrease height
+        val newX = EXCEEDS_MAX_HEIGHT_BOUNDS.right.toFloat() - 10
+        val newY = EXCEEDS_MAX_HEIGHT_BOUNDS.top.toFloat()
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is decreasing height closer to valid region.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_RIGHT or CTRL_TYPE_TOP,
+                repositionTaskBounds, EXCEEDS_MAX_HEIGHT_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.left)
+        assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.right - 10)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_HEIGHT_BOUNDS.bottom )
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_SIZE_CONSTRAINTS)
+    fun testMaxHeight_initialWidthMoreThanMax_decreasingBounds_resizeAllowed() {
+        val startingPoint = PointF(EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat(),
+            EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat())
+        val repositionTaskBounds = Rect(EXCEEDS_MAX_WIDTH_BOUNDS)
+
+        // Attempt to decrease width.
+        val newX = EXCEEDS_MAX_WIDTH_BOUNDS.left.toFloat() + 20
+        val newY = EXCEEDS_MAX_WIDTH_BOUNDS.top.toFloat()
+        val delta = DragPositioningCallbackUtility.calculateDelta(newX, newY, startingPoint)
+
+        // Resize should be allowed as drag is decreasing width closer to valid region.
+        assertTrue(
+            DragPositioningCallbackUtility.changeBounds(
+                CTRL_TYPE_LEFT or CTRL_TYPE_TOP,
+                repositionTaskBounds, EXCEEDS_MAX_WIDTH_BOUNDS, STABLE_BOUNDS, delta,
+                mockDisplayController, mockWindowDecoration
+            )
+        )
+
+        assertThat(repositionTaskBounds.left).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.left + 20)
+        assertThat(repositionTaskBounds.top).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.top)
+        assertThat(repositionTaskBounds.right).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.right)
+        assertThat(repositionTaskBounds.bottom).isEqualTo(EXCEEDS_MAX_WIDTH_BOUNDS.bottom)
+    }
+
     private fun initializeTaskInfo(taskMinWidth: Int = MIN_WIDTH, taskMinHeight: Int = MIN_HEIGHT) {
         mockWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply {
             taskId = TASK_ID
@@ -571,6 +792,10 @@
         private const val NAVBAR_HEIGHT = 50
         private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600)
         private val STARTING_BOUNDS = Rect(0, 0, 100, 100)
+        private val BELOW_MIN_WIDTH_BOUNDS = Rect(0, 0, 50, 100)
+        private val BELOW_MIN_HEIGHT_BOUNDS = Rect(0, 0, 100, 50)
+        private val EXCEEDS_MAX_WIDTH_BOUNDS = Rect(0, 0, 3000, 1500)
+        private val EXCEEDS_MAX_HEIGHT_BOUNDS = Rect(0, 0, 1000, 2000)
         private val OFF_CENTER_STARTING_BOUNDS = Rect(-100, -100, 10, 10)
         private val DISALLOWED_RESIZE_AREA = Rect(
             DISPLAY_BOUNDS.left,
diff --git a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg.xml b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg.xml
deleted file mode 100644
index 35517ea..0000000
--- a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-  ~ 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.
-  -->
-
-<shape
-    xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
-    android:shape="rectangle">
-    <solid android:color="?androidprv:attr/colorAccentPrimary" />
-    <corners android:radius="12dp" />
-</shape>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml
index 18696c6..91a95a5 100644
--- a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml
+++ b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple.xml
@@ -16,6 +16,12 @@
   -->
 
 <ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:color="?android:attr/colorControlHighlight">
-    <item android:drawable="@drawable/audio_sharing_rounded_bg"/>
+    <item>
+        <shape android:shape="rectangle">
+            <corners android:radius="4dp" />
+            <solid android:color="?androidprv:attr/colorAccentPrimary" />
+        </shape>
+    </item>
 </ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_bottom.xml b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_bottom.xml
new file mode 100644
index 0000000..cce8a75
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_bottom.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item>
+        <shape android:shape="rectangle">
+            <corners
+                android:bottomLeftRadius="12dp"
+                android:bottomRightRadius="12dp"
+                android:topLeftRadius="4dp"
+                android:topRightRadius="4dp" />
+            <solid android:color="?androidprv:attr/colorAccentPrimary" />
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_top.xml b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_top.xml
new file mode 100644
index 0000000..1404197
--- /dev/null
+++ b/packages/SettingsLib/res/drawable/audio_sharing_rounded_bg_ripple_top.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2024 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:color="?android:attr/colorControlHighlight">
+    <item>
+        <shape android:shape="rectangle">
+            <corners
+                android:bottomLeftRadius="4dp"
+                android:bottomRightRadius="4dp"
+                android:topLeftRadius="12dp"
+                android:topRightRadius="12dp" />
+            <solid android:color="?androidprv:attr/colorAccentPrimary" />
+        </shape>
+    </item>
+</ripple>
\ No newline at end of file
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index 63661f6..4f315a2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -74,23 +74,7 @@
             new AudioDeviceCallback() {
                 @Override
                 public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) {
-                    // Activate the last hot plugged valid input device, to match the output device
-                    // behavior.
-                    @AudioDeviceType int deviceTypeToActivate = mSelectedInputDeviceType;
-                    for (AudioDeviceInfo info : addedDevices) {
-                        if (InputMediaDevice.isSupportedInputDevice(info.getType())) {
-                            deviceTypeToActivate = info.getType();
-                        }
-                    }
-
-                    // Only activate if we find a different valid input device. e.g. if none of the
-                    // addedDevices is supported input device, we don't need to activate anything.
-                    if (mSelectedInputDeviceType != deviceTypeToActivate) {
-                        mSelectedInputDeviceType = deviceTypeToActivate;
-                        AudioDeviceAttributes deviceAttributes =
-                                createInputDeviceAttributes(mSelectedInputDeviceType);
-                        setPreferredDeviceForAllPresets(deviceAttributes);
-                    }
+                    applyDefaultSelectedTypeToAllPresets();
                 }
 
                 @Override
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
index d808a25..782cee2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -24,7 +24,6 @@
 import static org.mockito.Mockito.atLeast;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -139,18 +138,6 @@
                 /* address= */ "");
     }
 
-    private AudioDeviceAttributes getUsbHeadsetDeviceAttributes() {
-        return new AudioDeviceAttributes(
-                AudioDeviceAttributes.ROLE_INPUT,
-                AudioDeviceInfo.TYPE_USB_HEADSET,
-                /* address= */ "");
-    }
-
-    private AudioDeviceAttributes getHdmiDeviceAttributes() {
-        return new AudioDeviceAttributes(
-                AudioDeviceAttributes.ROLE_INPUT, AudioDeviceInfo.TYPE_HDMI, /* address= */ "");
-    }
-
     private void onPreferredDevicesForCapturePresetChanged(InputRouteManager inputRouteManager) {
         final List<AudioDeviceAttributes> audioDeviceAttributesList =
                 new ArrayList<AudioDeviceAttributes>();
@@ -316,47 +303,21 @@
     }
 
     @Test
-    public void onAudioDevicesAdded_shouldActivateAddedDevice() {
+    public void onAudioDevicesAdded_shouldApplyDefaultSelectedDeviceToAllPresets() {
         final AudioManager audioManager = mock(AudioManager.class);
+        AudioDeviceAttributes wiredHeadsetDeviceAttributes = getWiredHeadsetDeviceAttributes();
+        when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+                .thenReturn(Collections.singletonList(wiredHeadsetDeviceAttributes));
+
         InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
         AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()};
         inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
 
-        // The only added wired headset will be activated.
+        // Called twice, one after initiation, the other after onAudioDevicesAdded call.
+        verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES);
         for (@MediaRecorder.Source int preset : PRESETS) {
-            verify(audioManager, atLeast(1))
-                    .setPreferredDeviceForCapturePreset(preset, getWiredHeadsetDeviceAttributes());
-        }
-    }
-
-    @Test
-    public void onAudioDevicesAdded_shouldActivateLastAddedDevice() {
-        final AudioManager audioManager = mock(AudioManager.class);
-        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
-        AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockUsbHeadsetInfo()};
-        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
-
-        // When adding multiple valid input devices, the last added device (usb headset in this
-        // case) will be activated.
-        for (@MediaRecorder.Source int preset : PRESETS) {
-            verify(audioManager, never())
-                    .setPreferredDeviceForCapturePreset(preset, getWiredHeadsetDeviceAttributes());
-            verify(audioManager, atLeast(1))
-                    .setPreferredDeviceForCapturePreset(preset, getUsbHeadsetDeviceAttributes());
-        }
-    }
-
-    @Test
-    public void onAudioDevicesAdded_doNotActivateInvalidAddedDevice() {
-        final AudioManager audioManager = mock(AudioManager.class);
-        InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
-        AudioDeviceInfo[] devices = {mockHdmiInfo()};
-        inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
-
-        // Do not activate since HDMI is not a valid input device.
-        for (@MediaRecorder.Source int preset : PRESETS) {
-            verify(audioManager, never())
-                    .setPreferredDeviceForCapturePreset(preset, getHdmiDeviceAttributes());
+            verify(audioManager, atLeast(2))
+                    .setPreferredDeviceForCapturePreset(preset, wiredHeadsetDeviceAttributes);
         }
     }
 
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 1c29db1..7b8dddb 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1529,6 +1529,13 @@
 }
 
 flag {
+  name: "shade_window_goes_around"
+  namespace: "systemui"
+  description: "Enables the shade window to move between displays"
+  bug: "362719719"
+}
+
+flag {
    name: "media_projection_request_attribution_fix"
    namespace: "systemui"
    description: "Ensure MediaProjection consent requests are properly attributed"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt
index 6da06d0..cd2dd04 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/back/FlingOnBackAnimationCallback.kt
@@ -60,7 +60,7 @@
                         it.touchY,
                         progress / SCALE_FACTOR,
                         it.swipeEdge,
-                        it.frameTime,
+                        it.frameTimeMillis,
                     )
                 onBackProgressedCompat(backEvent)
             }
@@ -87,7 +87,7 @@
         }
         reset()
         if (predictiveBackTimestampApi()) {
-            downTime = backEvent.frameTime
+            downTime = backEvent.frameTimeMillis
         }
         onBackStartedCompat(backEvent)
     }
@@ -98,7 +98,7 @@
             velocityTracker.addMovement(
                 MotionEvent.obtain(
                     /* downTime */ downTime!!,
-                    /* eventTime */ backEvent.frameTime,
+                    /* eventTime */ backEvent.frameTimeMillis,
                     /* action */ ACTION_MOVE,
                     /* x */ interpolatedProgress * SCALE_FACTOR,
                     /* y */ 0f,
@@ -111,7 +111,7 @@
                     backEvent.touchY,
                     interpolatedProgress,
                     backEvent.swipeEdge,
-                    backEvent.frameTime,
+                    backEvent.frameTimeMillis,
                 )
         } else {
             lastBackEvent =
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
new file mode 100644
index 0000000..3115191
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInContainer.kt
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.ui.graphics
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableFloatStateOf
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawWithContent
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.drawscope.ContentDrawScope
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.clipPath
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.graphics.layer.GraphicsLayer
+import androidx.compose.ui.graphics.layer.drawLayer
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.layout
+import androidx.compose.ui.layout.positionInWindow
+import androidx.compose.ui.modifier.ModifierLocalModifierNode
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.requireDensity
+import androidx.compose.ui.node.requireGraphicsContext
+import androidx.compose.ui.node.requireLayoutCoordinates
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.util.fastForEach
+
+/**
+ * Define this as a container into which other composables can be drawn using [drawInContainer].
+ *
+ * The elements redirected to this container will be drawn above the content of this composable.
+ */
+fun Modifier.container(state: ContainerState): Modifier {
+    return layout { measurable, constraints ->
+            val p = measurable.measure(constraints)
+            layout(p.width, p.height) {
+                val coords = coordinates
+                if (coords != null && !isLookingAhead) {
+                    state.lastCoords = coords
+                }
+
+                p.place(0, 0)
+            }
+        }
+        .drawWithContent {
+            drawContent()
+            state.drawInOverlay(this)
+        }
+}
+
+/**
+ * Draw this composable into the container associated to [state].
+ *
+ * @param state the state of the container into which we should draw this composable.
+ * @param enabled whether the redirection of the drawing to the container is enabled.
+ * @param zIndex the z-index of the composable in the container.
+ * @param clipPath the clip path applied when drawing this composable into the container.
+ */
+fun Modifier.drawInContainer(
+    state: ContainerState,
+    enabled: () -> Boolean = { true },
+    zIndex: Float = 0f,
+    clipPath: (LayoutDirection, Density) -> Path? = { _, _ -> null },
+): Modifier {
+    return this.then(
+        DrawInContainerElement(
+            state = state,
+            enabled = enabled,
+            zIndex = zIndex,
+            clipPath = clipPath,
+        )
+    )
+}
+
+class ContainerState {
+    private var renderers = mutableStateListOf<LayerRenderer>()
+    internal var lastCoords: LayoutCoordinates? = null
+
+    internal fun onLayerRendererAttached(renderer: LayerRenderer) {
+        renderers.add(renderer)
+        renderers.sortBy { it.zIndex }
+    }
+
+    internal fun onLayerRendererDetached(renderer: LayerRenderer) {
+        renderers.remove(renderer)
+    }
+
+    internal fun drawInOverlay(drawScope: DrawScope) {
+        renderers.fastForEach { it.drawInOverlay(drawScope) }
+    }
+}
+
+internal interface LayerRenderer {
+    val zIndex: Float
+
+    fun drawInOverlay(drawScope: DrawScope)
+}
+
+private data class DrawInContainerElement(
+    var state: ContainerState,
+    var enabled: () -> Boolean,
+    val zIndex: Float,
+    val clipPath: (LayoutDirection, Density) -> Path?,
+) : ModifierNodeElement<DrawInContainerNode>() {
+    override fun create(): DrawInContainerNode {
+        return DrawInContainerNode(state, enabled, zIndex, clipPath)
+    }
+
+    override fun update(node: DrawInContainerNode) {
+        node.state = state
+        node.enabled = enabled
+        node.zIndex = zIndex
+        node.clipPath = clipPath
+    }
+}
+
+/**
+ * The implementation of [drawInContainer].
+ *
+ * Note: this was forked from AndroidX RenderInTransitionOverlayNodeElement.kt
+ * (http://shortn/_3dfSFPbm8f).
+ */
+internal class DrawInContainerNode(
+    var state: ContainerState,
+    var enabled: () -> Boolean = { true },
+    zIndex: Float = 0f,
+    var clipPath: (LayoutDirection, Density) -> Path? = { _, _ -> null },
+) : Modifier.Node(), DrawModifierNode, ModifierLocalModifierNode {
+    var zIndex by mutableFloatStateOf(zIndex)
+
+    private inner class LayerWithRenderer(val layer: GraphicsLayer) : LayerRenderer {
+        override val zIndex: Float
+            get() = this@DrawInContainerNode.zIndex
+
+        override fun drawInOverlay(drawScope: DrawScope) {
+            if (enabled()) {
+                with(drawScope) {
+                    val containerCoords =
+                        checkNotNull(state.lastCoords) { "container is not placed" }
+                    val (x, y) =
+                        requireLayoutCoordinates().positionInWindow() -
+                            containerCoords.positionInWindow()
+                    val clipPath = clipPath(layoutDirection, requireDensity())
+                    if (clipPath != null) {
+                        clipPath(clipPath) { translate(x, y) { drawLayer(layer) } }
+                    } else {
+                        translate(x, y) { drawLayer(layer) }
+                    }
+                }
+            }
+        }
+    }
+
+    // Render in-place logic. Depending on the result of `renderInOverlay()`, the content will
+    // either render in-place or in the overlay, but never in both places.
+    override fun ContentDrawScope.draw() {
+        val layer = requireNotNull(layer) { "Error: layer never initialized" }
+        layer.record { this@draw.drawContent() }
+        if (!enabled()) {
+            drawLayer(layer)
+        }
+    }
+
+    val layer: GraphicsLayer?
+        get() = layerWithRenderer?.layer
+
+    private var layerWithRenderer: LayerWithRenderer? = null
+
+    override fun onAttach() {
+        LayerWithRenderer(requireGraphicsContext().createGraphicsLayer()).let {
+            state.onLayerRendererAttached(it)
+            layerWithRenderer = it
+        }
+    }
+
+    override fun onDetach() {
+        layerWithRenderer?.let {
+            state.onLayerRendererDetached(it)
+            requireGraphicsContext().releaseGraphicsLayer(it.layer)
+        }
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt
new file mode 100644
index 0000000..f5c3a83
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/compose/ui/graphics/DrawInOverlay.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.ui.graphics
+
+import android.view.View
+import android.view.ViewGroupOverlay
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCompositionContext
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalView
+import androidx.compose.ui.unit.IntSize
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.lifecycle.findViewTreeViewModelStoreOwner
+import androidx.lifecycle.setViewTreeLifecycleOwner
+import androidx.lifecycle.setViewTreeViewModelStoreOwner
+import androidx.savedstate.findViewTreeSavedStateRegistryOwner
+import androidx.savedstate.setViewTreeSavedStateRegistryOwner
+
+/**
+ * Draw this composable in the [overlay][ViewGroupOverlay] of the [current ComposeView][LocalView].
+ */
+@Composable
+fun Modifier.drawInOverlay(): Modifier {
+    val containerState = remember { ContainerState() }
+    val context = LocalContext.current
+    val localView = LocalView.current
+    val compositionContext = rememberCompositionContext()
+    val displayMetrics = context.resources.displayMetrics
+    val displaySize = IntSize(displayMetrics.widthPixels, displayMetrics.heightPixels)
+
+    DisposableEffect(containerState, context, localView, compositionContext, displaySize) {
+        val overlay = localView.rootView.overlay as ViewGroupOverlay
+        val view =
+            ComposeView(context).apply {
+                setParentCompositionContext(compositionContext)
+
+                // Set the owners.
+                setViewTreeLifecycleOwner(localView.findViewTreeLifecycleOwner())
+                setViewTreeViewModelStoreOwner(localView.findViewTreeViewModelStoreOwner())
+                setViewTreeSavedStateRegistryOwner(localView.findViewTreeSavedStateRegistryOwner())
+
+                setContent { Box(Modifier.fillMaxSize().container(containerState)) }
+            }
+
+        overlay.add(view)
+
+        // Make the ComposeView as big as the display. We have to manually measure and layout the
+        // View given that there is no layout pass in Android overlays.
+        view.measure(
+            View.MeasureSpec.makeSafeMeasureSpec(displaySize.width, View.MeasureSpec.EXACTLY),
+            View.MeasureSpec.makeSafeMeasureSpec(displaySize.height, View.MeasureSpec.EXACTLY),
+        )
+        view.layout(0, 0, displaySize.width, displaySize.height)
+
+        onDispose { overlay.remove(view) }
+    }
+
+    return this.drawInContainer(containerState, enabled = { true })
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
index 6e883c2..9e20e7d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyboard.shortcut.data.repository
 
 import android.hardware.input.fakeInputManager
+import android.view.KeyEvent.KEYCODE_1
 import android.view.KeyEvent.KEYCODE_A
 import android.view.KeyEvent.KEYCODE_B
 import android.view.KeyEvent.KEYCODE_C
@@ -24,6 +25,7 @@
 import android.view.KeyEvent.KEYCODE_E
 import android.view.KeyEvent.KEYCODE_F
 import android.view.KeyEvent.KEYCODE_G
+import android.view.KeyEvent.META_FUNCTION_ON
 import android.view.KeyboardShortcutGroup
 import android.view.KeyboardShortcutInfo
 import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -87,6 +89,48 @@
     }
 
     @Test
+    fun categories_keycodeAndModifiersAreMappedSeparatelyWhenIdentical() =
+        testScope.runTest {
+            fakeSystemSource.setGroups(simpleGroup(simpleShortcutInfo(KEYCODE_1)))
+
+            helper.toggle(deviceId = 123)
+            val categories by collectLastValue(repo.categories)
+
+            val systemCategory = categories?.firstOrNull { it.type == ShortcutCategoryType.System }
+
+            // Keycode 0x8 should be translated to the Key 1 instead of modifier FN
+            // which has the same keycode.
+            val expectedCategory =
+                ShortcutCategory(
+                    type = ShortcutCategoryType.System,
+                    simpleSubCategory(simpleShortcut("1")),
+                )
+
+            assertThat(systemCategory).isEqualTo(expectedCategory)
+        }
+
+    @Test
+    fun categories_keyCodeAndModifierHaveSameCode_codesAreMappedCorrectly() =
+        testScope.runTest {
+            fakeSystemSource.setGroups(simpleGroup(simpleShortcutInfo(KEYCODE_1, META_FUNCTION_ON)))
+
+            helper.toggle(deviceId = 123)
+            val categories by collectLastValue(repo.categories)
+
+            val systemCategory = categories?.firstOrNull { it.type == ShortcutCategoryType.System }
+
+            // Keycode 0x8 should be translated to the Key 1 instead of modifier FN
+            // which has the same keycode. while modifier mask 0x8 should be translated to FN.
+            val expectedCategory =
+                ShortcutCategory(
+                    type = ShortcutCategoryType.System,
+                    simpleSubCategory(simpleShortcut("Fn", "1")),
+                )
+
+            assertThat(systemCategory).isEqualTo(expectedCategory)
+        }
+
+    @Test
     fun categories_multipleSubscribers_replaysExistingValueToNewSubscribers() =
         testScope.runTest {
             fakeSystemSource.setGroups(TestShortcuts.systemGroups)
@@ -111,24 +155,14 @@
         testScope.runTest {
             fakeSystemSource.setGroups(
                 listOf(
-                    simpleGroup(
-                        simpleShortcutInfo(KEYCODE_A),
-                        simpleShortcutInfo(KEYCODE_B),
-                    ),
-                    simpleGroup(
-                        simpleShortcutInfo(KEYCODE_C),
-                    ),
+                    simpleGroup(simpleShortcutInfo(KEYCODE_A), simpleShortcutInfo(KEYCODE_B)),
+                    simpleGroup(simpleShortcutInfo(KEYCODE_C)),
                 )
             )
             fakeMultiTaskingSource.setGroups(
                 listOf(
-                    simpleGroup(
-                        simpleShortcutInfo(KEYCODE_D),
-                    ),
-                    simpleGroup(
-                        simpleShortcutInfo(KEYCODE_E),
-                        simpleShortcutInfo(KEYCODE_F),
-                    ),
+                    simpleGroup(simpleShortcutInfo(KEYCODE_D)),
+                    simpleGroup(simpleShortcutInfo(KEYCODE_E), simpleShortcutInfo(KEYCODE_F)),
                 )
             )
             fakeAppCategoriesSource.setGroups(listOf(simpleGroup(simpleShortcutInfo(KEYCODE_G))))
@@ -144,16 +178,11 @@
                         listOf(
                             simpleSubCategory(simpleShortcut("B")),
                             simpleSubCategory(simpleShortcut("C")),
-                        )
+                        ),
                     ),
                     ShortcutCategory(
                         ShortcutCategoryType.MultiTasking,
-                        listOf(
-                            simpleSubCategory(
-                                simpleShortcut("E"),
-                                simpleShortcut("F"),
-                            ),
-                        )
+                        listOf(simpleSubCategory(simpleShortcut("E"), simpleShortcut("F"))),
                     ),
                 )
         }
@@ -164,14 +193,14 @@
     private fun simpleShortcut(vararg keys: String) =
         Shortcut(
             label = simpleShortcutLabel,
-            commands = listOf(ShortcutCommand(keys.map { ShortcutKey.Text(it) }))
+            commands = listOf(ShortcutCommand(keys.map { ShortcutKey.Text(it) })),
         )
 
     private fun simpleGroup(vararg shortcuts: KeyboardShortcutInfo) =
         KeyboardShortcutGroup(simpleGroupLabel, shortcuts.asList())
 
-    private fun simpleShortcutInfo(keyCode: Int = 0) =
-        KeyboardShortcutInfo(simpleShortcutLabel, keyCode, /* modifiers= */ 0)
+    private fun simpleShortcutInfo(keyCode: Int = 0, modifiers: Int = 0) =
+        KeyboardShortcutInfo(simpleShortcutLabel, keyCode, modifiers)
 
     private val simpleShortcutLabel = "shortcut label"
     private val simpleGroupLabel = "group label"
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt
index 29e9ba7..d9d8169 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt
@@ -20,6 +20,8 @@
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection.LEFT
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection.RIGHT
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.Finished
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
 import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
@@ -61,23 +63,46 @@
     }
 
     @Test
-    fun triggersProgressRelativeToDistance() {
-        assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f)
-        assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE / 2, expectedProgress = 0.5f)
-        assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE, expectedProgress = 1f)
-        assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE, expectedProgress = 1f)
+    fun triggersProgressRelativeToDistanceWhenSwipingLeft() {
+        assertProgressWhileMovingFingers(
+            deltaX = -SWIPE_DISTANCE / 2,
+            expected = InProgress(progress = 0.5f, direction = LEFT),
+        )
+        assertProgressWhileMovingFingers(
+            deltaX = -SWIPE_DISTANCE,
+            expected = InProgress(progress = 1f, direction = LEFT),
+        )
     }
 
-    private fun assertProgressWhileMovingFingers(deltaX: Float, expectedProgress: Float) {
+    @Test
+    fun triggersProgressRelativeToDistanceWhenSwipingRight() {
+        assertProgressWhileMovingFingers(
+            deltaX = SWIPE_DISTANCE / 2,
+            expected = InProgress(progress = 0.5f, direction = RIGHT),
+        )
+        assertProgressWhileMovingFingers(
+            deltaX = SWIPE_DISTANCE,
+            expected = InProgress(progress = 1f, direction = RIGHT),
+        )
+    }
+
+    private fun assertProgressWhileMovingFingers(deltaX: Float, expected: InProgress) {
         assertStateAfterEvents(
             events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaX = deltaX) },
-            expectedState = InProgress(progress = expectedProgress),
+            expectedState = expected,
         )
     }
 
     @Test
     fun triggeredProgressIsNoBiggerThanOne() {
-        assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE * 2, expectedProgress = 1f)
+        assertProgressWhileMovingFingers(
+            deltaX = SWIPE_DISTANCE * 2,
+            expected = InProgress(progress = 1f, direction = RIGHT),
+        )
+        assertProgressWhileMovingFingers(
+            deltaX = -SWIPE_DISTANCE * 2,
+            expected = InProgress(progress = 1f, direction = LEFT),
+        )
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
deleted file mode 100644
index faf01ed..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.ringer.ui.viewmodel
-
-import android.media.AudioManager.RINGER_MODE_NORMAL
-import android.media.AudioManager.RINGER_MODE_SILENT
-import android.media.AudioManager.RINGER_MODE_VIBRATE
-import android.media.AudioManager.STREAM_RING
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.settingslib.volume.shared.model.RingerMode
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.haptics.fakeVibratorHelper
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.plugins.fakeVolumeDialogController
-import com.android.systemui.testKosmos
-import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper
-class VolumeDialogRingerDrawerViewModelTest : SysuiTestCase() {
-
-    private val kosmos = testKosmos()
-    private val testScope = kosmos.testScope
-    private val controller = kosmos.fakeVolumeDialogController
-    private val vibratorHelper = kosmos.fakeVibratorHelper
-
-    private lateinit var underTest: VolumeDialogRingerDrawerViewModel
-
-    @Before
-    fun setUp() {
-        underTest = kosmos.volumeDialogRingerDrawerViewModel
-    }
-
-    @Test
-    fun onSelectedRingerNormalModeButtonClicked_openDrawer() =
-        testScope.runTest {
-            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
-            val normalRingerMode = RingerMode(RINGER_MODE_NORMAL)
-
-            setUpRingerModeAndOpenDrawer(normalRingerMode)
-
-            assertThat(ringerViewModel).isNotNull()
-            assertThat(ringerViewModel?.drawerState)
-                .isEqualTo(RingerDrawerState.Open(normalRingerMode))
-        }
-
-    @Test
-    fun onSelectedRingerButtonClicked_drawerOpened_closeDrawer() =
-        testScope.runTest {
-            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
-            val normalRingerMode = RingerMode(RINGER_MODE_NORMAL)
-
-            setUpRingerModeAndOpenDrawer(normalRingerMode)
-            underTest.onRingerButtonClicked(normalRingerMode)
-            controller.getState()
-
-            assertThat(ringerViewModel).isNotNull()
-            assertThat(ringerViewModel?.drawerState)
-                .isEqualTo(RingerDrawerState.Closed(normalRingerMode))
-        }
-
-    @Test
-    fun onNewRingerButtonClicked_drawerOpened_updateRingerMode_closeDrawer() =
-        testScope.runTest {
-            val ringerViewModel by collectLastValue(underTest.ringerViewModel)
-            val vibrateRingerMode = RingerMode(RINGER_MODE_VIBRATE)
-
-            setUpRingerModeAndOpenDrawer(RingerMode(RINGER_MODE_NORMAL))
-            // Select vibrate ringer mode.
-            underTest.onRingerButtonClicked(vibrateRingerMode)
-            controller.getState()
-            runCurrent()
-
-            assertThat(ringerViewModel).isNotNull()
-            assertThat(
-                    ringerViewModel
-                        ?.availableButtons
-                        ?.get(ringerViewModel!!.currentButtonIndex)
-                        ?.ringerMode
-                )
-                .isEqualTo(vibrateRingerMode)
-            assertThat(ringerViewModel?.drawerState)
-                .isEqualTo(RingerDrawerState.Closed(vibrateRingerMode))
-
-            val silentRingerMode = RingerMode(RINGER_MODE_SILENT)
-            // Open drawer
-            underTest.onRingerButtonClicked(vibrateRingerMode)
-            controller.getState()
-
-            // Select silent ringer mode.
-            underTest.onRingerButtonClicked(silentRingerMode)
-            controller.getState()
-            runCurrent()
-
-            assertThat(ringerViewModel).isNotNull()
-            assertThat(
-                    ringerViewModel
-                        ?.availableButtons
-                        ?.get(ringerViewModel!!.currentButtonIndex)
-                        ?.ringerMode
-                )
-                .isEqualTo(silentRingerMode)
-            assertThat(ringerViewModel?.drawerState)
-                .isEqualTo(RingerDrawerState.Closed(silentRingerMode))
-            assertThat(controller.hasScheduledTouchFeedback).isFalse()
-            assertThat(vibratorHelper.totalVibrations).isEqualTo(2)
-        }
-
-    private fun TestScope.setUpRingerModeAndOpenDrawer(selectedRingerMode: RingerMode) {
-        controller.setStreamVolume(STREAM_RING, 50)
-        controller.setRingerMode(selectedRingerMode.value, false)
-        runCurrent()
-
-        underTest.onRingerButtonClicked(RingerMode(selectedRingerMode.value))
-        controller.getState()
-        runCurrent()
-    }
-}
diff --git a/packages/SystemUI/res/layout/contextual_edu_dialog.xml b/packages/SystemUI/res/layout/contextual_edu_dialog.xml
index ee42b23..7eb6efe 100644
--- a/packages/SystemUI/res/layout/contextual_edu_dialog.xml
+++ b/packages/SystemUI/res/layout/contextual_edu_dialog.xml
@@ -40,6 +40,5 @@
         android:fontFamily="google-sans-medium"
         android:maxWidth="280dp"
         android:textColor="?androidprv:attr/materialColorOnTertiaryFixed"
-        android:textSize="14sp"
-        android:textStyle="bold" />
+        android:textSize="14sp" />
 </LinearLayout>
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
index 85bd0b0..a085887 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt
@@ -68,7 +68,7 @@
     @InputShortcuts private val inputShortcutsSource: KeyboardShortcutGroupsSource,
     @CurrentAppShortcuts private val currentAppShortcutsSource: KeyboardShortcutGroupsSource,
     private val inputManager: InputManager,
-    stateRepository: ShortcutHelperStateRepository
+    stateRepository: ShortcutHelperStateRepository,
 ) {
 
     private val sources =
@@ -76,27 +76,27 @@
             InternalGroupsSource(
                 source = systemShortcutsSource,
                 isTrusted = true,
-                typeProvider = { System }
+                typeProvider = { System },
             ),
             InternalGroupsSource(
                 source = multitaskingShortcutsSource,
                 isTrusted = true,
-                typeProvider = { MultiTasking }
+                typeProvider = { MultiTasking },
             ),
             InternalGroupsSource(
                 source = appCategoriesShortcutsSource,
                 isTrusted = true,
-                typeProvider = { AppCategories }
+                typeProvider = { AppCategories },
             ),
             InternalGroupsSource(
                 source = inputShortcutsSource,
                 isTrusted = false,
-                typeProvider = { InputMethodEditor }
+                typeProvider = { InputMethodEditor },
             ),
             InternalGroupsSource(
                 source = currentAppShortcutsSource,
                 isTrusted = false,
-                typeProvider = { groups -> getCurrentAppShortcutCategoryType(groups) }
+                typeProvider = { groups -> getCurrentAppShortcutCategoryType(groups) },
             ),
         )
 
@@ -179,7 +179,7 @@
                             shortcutGroup.items,
                             keepIcons,
                             supportedKeyCodes,
-                        )
+                        ),
                     )
                 }
                 .filter { it.shortcuts.isNotEmpty() }
@@ -214,13 +214,13 @@
         return Shortcut(
             label = shortcutInfo.label!!.toString(),
             icon = toShortcutIcon(keepIcon, shortcutInfo),
-            commands = listOf(shortcutCommand)
+            commands = listOf(shortcutCommand),
         )
     }
 
     private fun toShortcutIcon(
         keepIcon: Boolean,
-        shortcutInfo: KeyboardShortcutInfo
+        shortcutInfo: KeyboardShortcutInfo,
     ): ShortcutIcon? {
         if (!keepIcon) {
             return null
@@ -236,13 +236,13 @@
 
     private fun toShortcutCommand(
         keyCharacterMap: KeyCharacterMap,
-        info: KeyboardShortcutInfo
+        info: KeyboardShortcutInfo,
     ): ShortcutCommand? {
         val keys = mutableListOf<ShortcutKey>()
         var remainingModifiers = info.modifiers
         SUPPORTED_MODIFIERS.forEach { supportedModifier ->
             if ((supportedModifier and remainingModifiers) != 0) {
-                keys += toShortcutKey(keyCharacterMap, supportedModifier) ?: return null
+                keys += toShortcutModifierKey(supportedModifier) ?: return null
                 // "Remove" the modifier from the remaining modifiers
                 remainingModifiers = remainingModifiers and supportedModifier.inv()
             }
@@ -262,6 +262,20 @@
         return ShortcutCommand(keys)
     }
 
+    private fun toShortcutModifierKey(modifierMask: Int): ShortcutKey? {
+        val iconResId = ShortcutHelperKeys.keyIcons[modifierMask]
+        if (iconResId != null) {
+            return ShortcutKey.Icon(iconResId)
+        }
+
+        val modifierLabel = ShortcutHelperKeys.modifierLabels[modifierMask]
+        if (modifierLabel != null) {
+            return ShortcutKey.Text(modifierLabel(context))
+        }
+        Log.wtf("TAG", "Couldn't find label or icon for modifier $modifierMask")
+        return null
+    }
+
     private fun toShortcutKey(
         keyCharacterMap: KeyCharacterMap,
         keyCode: Int,
@@ -289,7 +303,7 @@
 
     private suspend fun fetchSupportedKeyCodes(
         deviceId: Int,
-        groupsFromAllSources: List<List<KeyboardShortcutGroup>>
+        groupsFromAllSources: List<List<KeyboardShortcutGroup>>,
     ): Set<Int> =
         withContext(backgroundDispatcher) {
             val allUsedKeyCodes =
@@ -320,7 +334,7 @@
                 KeyEvent.META_ALT_ON,
                 KeyEvent.META_SHIFT_ON,
                 KeyEvent.META_SYM_ON,
-                KeyEvent.META_FUNCTION_ON
+                KeyEvent.META_FUNCTION_ON,
             )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt
index 8db16fa..288efa2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt
@@ -124,6 +124,17 @@
             KEYCODE_RECENT_APPS to R.drawable.ic_check_box_outline_blank,
         )
 
+    val modifierLabels =
+        mapOf<Int, (Context) -> String>(
+            // Modifiers
+            META_META_ON to { "Meta" },
+            META_CTRL_ON to { "Ctrl" },
+            META_ALT_ON to { "Alt" },
+            META_SHIFT_ON to { "Shift" },
+            META_SYM_ON to { "Sym" },
+            META_FUNCTION_ON to { "Fn" },
+        )
+
     val specialKeyLabels =
         mapOf<Int, (Context) -> String>(
             KEYCODE_HOME to { context -> context.getString(R.string.keyboard_key_home) },
@@ -317,7 +328,7 @@
                 { context ->
                     context.getString(
                         R.string.keyboard_key_numpad_template,
-                        context.getString(R.string.keyboard_key_enter)
+                        context.getString(R.string.keyboard_key_enter),
                     )
                 },
             KEYCODE_NUMPAD_EQUALS to
@@ -343,13 +354,5 @@
             KEYCODE_CTRL_RIGHT to { "Ctrl" },
             KEYCODE_SHIFT_LEFT to { "Shift" },
             KEYCODE_SHIFT_RIGHT to { "Shift" },
-
-            // Modifiers
-            META_META_ON to { "Meta" },
-            META_CTRL_ON to { "Ctrl" },
-            META_ALT_ON to { "Alt" },
-            META_SHIFT_ON to { "Shift" },
-            META_SYM_ON to { "Sym" },
-            META_FUNCTION_ON to { "Fn" },
         )
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
new file mode 100644
index 0000000..c72db56
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeDisplayAwareModule.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade
+
+import android.content.Context
+import android.content.res.Resources
+import android.view.LayoutInflater
+import android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
+import com.android.systemui.Flags
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.res.R
+import dagger.Module
+import dagger.Provides
+
+/**
+ * Module responsible for managing display-specific components and resources for the notification
+ * shade window.
+ *
+ * This isolation is crucial because when the window transitions between displays, its associated
+ * context, resources, and display characteristics (like density and size) also change. If the shade
+ * window shared the same context as the rest of the system UI, it could lead to inconsistencies and
+ * errors due to incorrect display information.
+ *
+ * By using this dedicated module, we ensure the notification shade window always utilizes the
+ * correct display context and resources, regardless of the display it's on.
+ */
+@Module
+object ShadeDisplayAwareModule {
+
+    /** Creates a new context for the shade window. */
+    @Provides
+    @ShadeDisplayAware
+    @SysUISingleton
+    fun provideShadeDisplayAwareContext(context: Context): Context {
+        return if (Flags.shadeWindowGoesAround()) {
+            context
+                .createWindowContext(context.display, TYPE_APPLICATION_OVERLAY, /* options= */ null)
+                .apply { setTheme(R.style.Theme_SystemUI) }
+        } else {
+            context
+        }
+    }
+
+    @Provides
+    @ShadeDisplayAware
+    @SysUISingleton
+    fun provideShadeDisplayAwareResources(@ShadeDisplayAware context: Context): Resources {
+        return context.resources
+    }
+
+    @Provides
+    @ShadeDisplayAware
+    @SysUISingleton
+    fun providesDisplayAwareLayoutInflater(@ShadeDisplayAware context: Context): LayoutInflater {
+        return LayoutInflater.from(context)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 2348a11..6f5547a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -49,7 +49,10 @@
 import javax.inject.Provider
 
 /** Module for classes related to the notification shade. */
-@Module(includes = [StartShadeModule::class, ShadeViewProviderModule::class])
+@Module(
+    includes =
+        [StartShadeModule::class, ShadeViewProviderModule::class, ShadeDisplayAwareModule::class]
+)
 abstract class ShadeModule {
     companion object {
         @Provides
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index ec1dc0a7..87b16ef 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -1574,7 +1574,7 @@
         if (mMaxDisplayedNotifications != -1) {
             // The stack intrinsic height already contains the correct value when there is a limit
             // in the max number of notifications (e.g. as in keyguard).
-            height = mIntrinsicContentHeight;
+            height = mScrollViewFields.getIntrinsicStackHeight();
         } else {
             height = Math.max(0f, mAmbientState.getStackCutoff() - mAmbientState.getStackTop());
         }
@@ -2610,7 +2610,7 @@
     }
 
     @VisibleForTesting
-    void updateStackHeight() {
+    void updateIntrinsicStackHeight() {
         if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return;
 
         final int shelfIntrinsicHeight = mShelf != null ? mShelf.getIntrinsicHeight() : 0;
@@ -2621,8 +2621,11 @@
                 mMaxDisplayedNotifications,
                 shelfIntrinsicHeight
         );
-        mIntrinsicContentHeight = notificationsHeight;
-        final int fullStackHeight = notificationsHeight + footerIntrinsicHeight + mBottomPadding;
+        // When there is a limit in the max number of notifications, we never display the footer.
+        final int fullStackHeight = mMaxDisplayedNotifications != -1
+                ? notificationsHeight
+                : notificationsHeight + footerIntrinsicHeight + mBottomPadding;
+
         if (mScrollViewFields.getIntrinsicStackHeight() != fullStackHeight) {
             mScrollViewFields.setIntrinsicStackHeight(fullStackHeight);
             notifyStackHeightChangedListeners();
@@ -2631,7 +2634,7 @@
 
     private void updateContentHeight() {
         if (SceneContainerFlag.isEnabled()) {
-            updateStackHeight();
+            updateIntrinsicStackHeight();
             return;
         }
 
@@ -5348,7 +5351,12 @@
     public void setMaxDisplayedNotifications(int maxDisplayedNotifications) {
         if (mMaxDisplayedNotifications != maxDisplayedNotifications) {
             mMaxDisplayedNotifications = maxDisplayedNotifications;
-            updateContentHeight();
+            if (SceneContainerFlag.isEnabled()) {
+                updateIntrinsicStackHeight();
+                updateStackEndHeightAndStackHeight(mAmbientState.getExpansionFraction());
+            } else {
+                updateContentHeight();
+            }
             notifyHeightChangeListener(mShelf);
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
index 024048c..35ea0ea 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt
@@ -18,6 +18,9 @@
 
 import android.util.MathUtils
 import android.view.MotionEvent
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection.LEFT
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection.RIGHT
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
 import kotlin.math.abs
 
 /**
@@ -44,7 +47,13 @@
             gestureStateChangedCallback,
             gestureState,
             isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx },
-            progress = { MathUtils.saturate(abs(it.deltaX / gestureDistanceThresholdPx)) },
+            progress = ::getProgress,
         )
     }
+
+    private fun getProgress(it: Moving): InProgress {
+        val direction = if (it.deltaX > 0) RIGHT else LEFT
+        val value = MathUtils.saturate(abs(it.deltaX / gestureDistanceThresholdPx))
+        return InProgress(value, direction)
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
index b513c49..f27ddb5 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureState.kt
@@ -21,5 +21,11 @@
 
     data object Finished : GestureState
 
-    data class InProgress(val progress: Float = 0f) : GestureState
+    data class InProgress(val progress: Float = 0f, val direction: GestureDirection? = null) :
+        GestureState
+}
+
+enum class GestureDirection {
+    LEFT,
+    RIGHT,
 }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
index f194677..24f5d1f 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
@@ -21,7 +21,7 @@
     gestureStateChangedCallback: (GestureState) -> Unit,
     gestureState: DistanceGestureState?,
     isFinished: (Finished) -> Boolean,
-    progress: (Moving) -> Float,
+    progress: (Moving) -> GestureState.InProgress,
 ) {
     when (gestureState) {
         is Finished -> {
@@ -32,7 +32,7 @@
             }
         }
         is Moving -> {
-            gestureStateChangedCallback(GestureState.InProgress(progress(gestureState)))
+            gestureStateChangedCallback(progress(gestureState))
         }
         is Started -> gestureStateChangedCallback(GestureState.InProgress())
         else -> {}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
index b804b9a..e10b825 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt
@@ -18,6 +18,7 @@
 
 import android.util.MathUtils
 import android.view.MotionEvent
+import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.InProgress
 
 /** Recognizes touchpad home gesture, that is - using three fingers on touchpad - swiping up. */
 class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer {
@@ -40,7 +41,7 @@
             gestureStateChangedCallback,
             gestureState,
             isFinished = { -it.deltaY >= gestureDistanceThresholdPx },
-            progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) },
+            progress = { InProgress(MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx)) },
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
index 7d484ee..c478886 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt
@@ -54,7 +54,9 @@
                 -state.deltaY >= gestureDistanceThresholdPx &&
                     abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs
             },
-            progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) },
+            progress = {
+                GestureState.InProgress(MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx))
+            },
         )
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerButtonViewModel.kt
deleted file mode 100644
index 78d2d16..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerButtonViewModel.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.ringer.ui.viewmodel
-
-import android.annotation.DrawableRes
-import android.annotation.StringRes
-import com.android.settingslib.volume.shared.model.RingerMode
-
-/** Models ringer button that corresponds to each ringer mode. */
-data class RingerButtonViewModel(
-    /** Image resource id for the image button. */
-    @DrawableRes val imageResId: Int,
-    /** Content description for a11y. */
-    @StringRes val contentDescriptionResId: Int,
-    /** Hint label for accessibility use. */
-    @StringRes val hintLabelResId: Int,
-    /** Used to notify view model when button is clicked. */
-    val ringerMode: RingerMode,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.kt
deleted file mode 100644
index f321837..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.ringer.ui.viewmodel
-
-import com.android.settingslib.volume.shared.model.RingerMode
-
-/** Models volume dialog ringer drawer state */
-sealed interface RingerDrawerState {
-
-    /** When clicked to open drawer */
-    data class Open(val mode: RingerMode) : RingerDrawerState
-
-    /** When clicked to close drawer */
-    data class Closed(val mode: RingerMode) : RingerDrawerState
-
-    /** Initial state when volume dialog is shown with a closed drawer. */
-    interface Initial : RingerDrawerState {
-        companion object : Initial
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt
deleted file mode 100644
index a09bfeb..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.ringer.ui.viewmodel
-
-/** Models volume dialog ringer */
-data class RingerViewModel(
-    /** List of the available buttons according to the available modes */
-    val availableButtons: List<RingerButtonViewModel?>,
-    /** The index of the currently selected button */
-    val currentButtonIndex: Int,
-    /** For open and close animations */
-    val drawerState: RingerDrawerState,
-)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
deleted file mode 100644
index ac82ae3..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.ringer.ui.viewmodel
-
-import android.media.AudioAttributes
-import android.media.AudioManager.RINGER_MODE_NORMAL
-import android.media.AudioManager.RINGER_MODE_SILENT
-import android.media.AudioManager.RINGER_MODE_VIBRATE
-import android.os.VibrationEffect
-import com.android.settingslib.volume.shared.model.RingerMode
-import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.VibratorHelper
-import com.android.systemui.volume.Events
-import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
-import com.android.systemui.volume.dialog.ringer.domain.VolumeDialogRingerInteractor
-import com.android.systemui.volume.dialog.ringer.shared.model.VolumeDialogRingerModel
-import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
-import dagger.assisted.AssistedFactory
-import dagger.assisted.AssistedInject
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.stateIn
-
-private const val TAG = "VolumeDialogRingerDrawerViewModel"
-
-class VolumeDialogRingerDrawerViewModel
-@AssistedInject
-constructor(
-    @VolumeDialog private val coroutineScope: CoroutineScope,
-    @Background private val backgroundDispatcher: CoroutineDispatcher,
-    private val interactor: VolumeDialogRingerInteractor,
-    private val vibrator: VibratorHelper,
-    private val volumeDialogLogger: VolumeDialogLogger,
-) {
-
-    private val drawerState = MutableStateFlow<RingerDrawerState>(RingerDrawerState.Initial)
-
-    val ringerViewModel: Flow<RingerViewModel> =
-        combine(interactor.ringerModel, drawerState) { ringerModel, state ->
-                ringerModel.toViewModel(state)
-            }
-            .flowOn(backgroundDispatcher)
-            .stateIn(coroutineScope, SharingStarted.Eagerly, null)
-            .filterNotNull()
-
-    // Vibration attributes.
-    private val sonificiationVibrationAttributes =
-        AudioAttributes.Builder()
-            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
-            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
-            .build()
-
-    fun onRingerButtonClicked(ringerMode: RingerMode) {
-        if (drawerState.value is RingerDrawerState.Open) {
-            Events.writeEvent(Events.EVENT_RINGER_TOGGLE, ringerMode.value)
-            provideTouchFeedback(ringerMode)
-            interactor.setRingerMode(ringerMode)
-        }
-        drawerState.value =
-            when (drawerState.value) {
-                is RingerDrawerState.Initial -> {
-                    RingerDrawerState.Open(ringerMode)
-                }
-                is RingerDrawerState.Open -> {
-                    RingerDrawerState.Closed(ringerMode)
-                }
-                is RingerDrawerState.Closed -> {
-                    RingerDrawerState.Open(ringerMode)
-                }
-            }
-    }
-
-    private fun provideTouchFeedback(ringerMode: RingerMode) {
-        when (ringerMode.value) {
-            RINGER_MODE_NORMAL -> {
-                interactor.scheduleTouchFeedback()
-                null
-            }
-            RINGER_MODE_SILENT -> VibrationEffect.get(VibrationEffect.EFFECT_CLICK)
-            RINGER_MODE_VIBRATE -> VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)
-            else -> VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK)
-        }?.let { vibrator.vibrate(it, sonificiationVibrationAttributes) }
-    }
-
-    private fun VolumeDialogRingerModel.toViewModel(
-        drawerState: RingerDrawerState
-    ): RingerViewModel {
-        val currentIndex = availableModes.indexOf(currentRingerMode)
-        if (currentIndex == -1) {
-            volumeDialogLogger.onCurrentRingerModeIsUnsupported(currentRingerMode)
-        }
-        return RingerViewModel(
-            availableButtons = availableModes.map { mode -> toButtonViewModel(mode) },
-            currentButtonIndex = currentIndex,
-            drawerState = drawerState,
-        )
-    }
-
-    private fun VolumeDialogRingerModel.toButtonViewModel(
-        ringerMode: RingerMode
-    ): RingerButtonViewModel? {
-        return when (ringerMode.value) {
-            RINGER_MODE_SILENT ->
-                RingerButtonViewModel(
-                    imageResId = R.drawable.ic_speaker_mute,
-                    contentDescriptionResId = R.string.volume_ringer_status_silent,
-                    hintLabelResId = R.string.volume_ringer_hint_unmute,
-                    ringerMode = ringerMode,
-                )
-            RINGER_MODE_VIBRATE ->
-                RingerButtonViewModel(
-                    imageResId = R.drawable.ic_volume_ringer_vibrate,
-                    contentDescriptionResId = R.string.volume_ringer_status_vibrate,
-                    hintLabelResId = R.string.volume_ringer_hint_vibrate,
-                    ringerMode = ringerMode,
-                )
-            RINGER_MODE_NORMAL ->
-                when {
-                    isMuted && isEnabled ->
-                        RingerButtonViewModel(
-                            imageResId = R.drawable.ic_speaker_mute,
-                            contentDescriptionResId = R.string.volume_ringer_status_normal,
-                            hintLabelResId = R.string.volume_ringer_hint_unmute,
-                            ringerMode = ringerMode,
-                        )
-
-                    availableModes.contains(RingerMode(RINGER_MODE_VIBRATE)) ->
-                        RingerButtonViewModel(
-                            imageResId = R.drawable.ic_speaker_on,
-                            contentDescriptionResId = R.string.volume_ringer_status_normal,
-                            hintLabelResId = R.string.volume_ringer_hint_vibrate,
-                            ringerMode = ringerMode,
-                        )
-
-                    else ->
-                        RingerButtonViewModel(
-                            imageResId = R.drawable.ic_speaker_on,
-                            contentDescriptionResId = R.string.volume_ringer_status_normal,
-                            hintLabelResId = R.string.volume_ringer_hint_mute,
-                            ringerMode = ringerMode,
-                        )
-                }
-            else -> null
-        }
-    }
-
-    @AssistedFactory
-    interface Factory {
-        fun create(): VolumeDialogRingerDrawerViewModel
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
index 9a3aa7e..59c38c0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/VolumeDialogLogger.kt
@@ -15,7 +15,6 @@
  */
 package com.android.systemui.volume.dialog.shared
 
-import com.android.settingslib.volume.shared.model.RingerMode
 import com.android.systemui.log.LogBuffer
 import com.android.systemui.log.core.LogLevel
 import com.android.systemui.log.dagger.VolumeLog
@@ -44,13 +43,4 @@
             { "Dismiss: ${Events.DISMISS_REASONS[int1]}" },
         )
     }
-
-    fun onCurrentRingerModeIsUnsupported(ringerMode: RingerMode) {
-        logBuffer.log(
-            TAG,
-            LogLevel.DEBUG,
-            { int1 = ringerMode.value },
-            { "Current ringer mode: $int1, ringer mode is unsupported in ringer drawer options" },
-        )
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
index 4eae3b9a..c7f5801 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
@@ -17,8 +17,11 @@
 package com.android.systemui.volume.dialog.ui.utils
 
 import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
 import android.view.ViewPropertyAnimator
 import kotlin.coroutines.resume
+import kotlinx.coroutines.CancellableContinuation
 import kotlinx.coroutines.suspendCancellableCoroutine
 
 /**
@@ -39,11 +42,12 @@
             }
 
             override fun onAnimationEnd(animation: Animator) {
-                continuation.resume(Unit)
+                continuation.resumeIfCan(Unit)
                 animationListener?.onAnimationEnd(animation)
             }
 
             override fun onAnimationCancel(animation: Animator) {
+                continuation.resumeIfCan(Unit)
                 animationListener?.onAnimationCancel(animation)
             }
 
@@ -54,3 +58,30 @@
     )
     continuation.invokeOnCancellation { this.cancel() }
 }
+
+/**
+ * Starts animation and suspends until it's finished. Cancels the animation if the running coroutine
+ * is cancelled.
+ */
+@Suppress("UNCHECKED_CAST")
+suspend fun <T> ValueAnimator.awaitAnimation(onValueChanged: (T) -> Unit) {
+    suspendCancellableCoroutine { continuation ->
+        addListener(
+            object : AnimatorListenerAdapter() {
+                override fun onAnimationEnd(animation: Animator) = continuation.resumeIfCan(Unit)
+
+                override fun onAnimationCancel(animation: Animator) = continuation.resumeIfCan(Unit)
+            }
+        )
+        addUpdateListener { onValueChanged(it.animatedValue as T) }
+
+        start()
+        continuation.invokeOnCancellation { cancel() }
+    }
+}
+
+private fun <T> CancellableContinuation<T>.resumeIfCan(value: T) {
+    if (!isCancelled && !isCompleted) {
+        resume(value)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 87cda64..fdfc253 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -244,7 +244,7 @@
         when(mStackSizeCalculator.computeHeight(eq(mStackScroller), anyInt(), anyFloat()))
                 .thenReturn((float) stackHeight);
 
-        mStackScroller.updateStackHeight();
+        mStackScroller.updateIntrinsicStackHeight();
 
         assertThat(mStackScroller.getIntrinsicStackHeight()).isEqualTo(stackHeight);
     }
@@ -1218,7 +1218,38 @@
     }
 
     @Test
-    @DisableSceneContainer // TODO(b/312473478): address disabled test
+    @EnableSceneContainer
+    public void testSetMaxDisplayedNotifications_updatesStackHeight() {
+        int fullStackHeight = 300;
+        int limitedStackHeight = 100;
+        int maxNotifs = 2; // any non-zero limit
+        float stackTop = 100;
+        float stackCutoff = 1100;
+        float stackViewPortHeight = stackCutoff - stackTop;
+        mStackScroller.setStackTop(stackTop);
+        mStackScroller.setStackCutoff(stackCutoff);
+        when(mStackSizeCalculator.computeHeight(eq(mStackScroller), eq(-1), anyFloat()))
+                .thenReturn((float) fullStackHeight);
+        when(mStackSizeCalculator.computeHeight(eq(mStackScroller), eq(maxNotifs), anyFloat()))
+                .thenReturn((float) limitedStackHeight);
+
+        // When we set a limit on max displayed notifications
+        mStackScroller.setMaxDisplayedNotifications(maxNotifs);
+
+        // Then
+        assertThat(mStackScroller.getIntrinsicStackHeight()).isEqualTo(limitedStackHeight);
+        assertThat(mAmbientState.getStackEndHeight()).isEqualTo(limitedStackHeight);
+
+        // When there is no limit on max displayed notifications
+        mStackScroller.setMaxDisplayedNotifications(-1);
+
+        // Then
+        assertThat(mStackScroller.getIntrinsicStackHeight()).isEqualTo(fullStackHeight);
+        assertThat(mAmbientState.getStackEndHeight()).isEqualTo(stackViewPortHeight);
+    }
+
+    @Test
+    @DisableSceneContainer
     public void testSetMaxDisplayedNotifications_notifiesListeners() {
         ExpandableView.OnHeightChangedListener listener =
                 mock(ExpandableView.OnHeightChangedListener.class);
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
deleted file mode 100644
index db1c01a..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelKosmos.kt
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.ringer.ui.viewmodel
-
-import com.android.systemui.haptics.vibratorHelper
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.volume.dialog.ringer.domain.volumeDialogRingerInteractor
-import com.android.systemui.volume.dialog.shared.volumeDialogLogger
-
-val Kosmos.volumeDialogRingerDrawerViewModel by
-    Kosmos.Fixture {
-        VolumeDialogRingerDrawerViewModel(
-            backgroundDispatcher = testDispatcher,
-            coroutineScope = applicationCoroutineScope,
-            interactor = volumeDialogRingerInteractor,
-            vibrator = vibratorHelper,
-            volumeDialogLogger = volumeDialogLogger,
-        )
-    }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/shared/VolumeDialogLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/shared/VolumeDialogLoggerKosmos.kt
deleted file mode 100644
index f9d4a99..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/shared/VolumeDialogLoggerKosmos.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.volume.dialog.shared
-
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.log.logcatLogBuffer
-
-val Kosmos.volumeDialogLogger by Kosmos.Fixture { VolumeDialogLogger(logcatLogBuffer()) }