Merge "Don't pilfer pointers with 3-button-nav predictive back" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 88180e5..1b42977 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -20809,6 +20809,7 @@
     method public int describeContents();
     method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @FloatRange(from=0.0f, to=1.0f) public float getDefaultBrightness();
     method public int getDensityDpi();
+    method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @FloatRange(from=0.0f, to=1.0f) public float getDimBrightness();
     method @NonNull public java.util.Set<java.lang.String> getDisplayCategories();
     method public int getFlags();
     method public int getHeight();
@@ -20830,6 +20831,7 @@
     method @NonNull public android.hardware.display.VirtualDisplayConfig build();
     method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setBrightnessListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.display.VirtualDisplayConfig.BrightnessListener);
     method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDefaultBrightness(@FloatRange(from=0.0f, to=1.0f) float);
+    method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDimBrightness(@FloatRange(from=0.0f, to=1.0f) float);
     method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDisplayCategories(@NonNull java.util.Set<java.lang.String>);
     method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setFlags(int);
     method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setRequestedRefreshRate(@FloatRange(from=0.0f) float);
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index eceaa8f..7257055 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -66,6 +66,7 @@
     private final DisplayCutout mDisplayCutout;
     private final boolean mIgnoreActivitySizeRestrictions;
     private final float mDefaultBrightness;
+    private final float mDimBrightness;
     private final IBrightnessListener mBrightnessListener;
 
     private VirtualDisplayConfig(
@@ -84,6 +85,7 @@
             @Nullable DisplayCutout displayCutout,
             boolean ignoreActivitySizeRestrictions,
             @FloatRange(from = 0.0f, to = 1.0f) float defaultBrightness,
+            @FloatRange(from = 0.0f, to = 1.0f) float dimBrightness,
             IBrightnessListener brightnessListener) {
         mName = name;
         mWidth = width;
@@ -100,6 +102,7 @@
         mDisplayCutout = displayCutout;
         mIgnoreActivitySizeRestrictions = ignoreActivitySizeRestrictions;
         mDefaultBrightness = defaultBrightness;
+        mDimBrightness = dimBrightness;
         mBrightnessListener = brightnessListener;
     }
 
@@ -180,6 +183,19 @@
     }
 
     /**
+     * Returns the dim brightness of the display.
+     *
+     * <p>Value of {@code 0.0} indicates the minimum supported brightness and value of {@code 1.0}
+     * indicates the maximum supported brightness.</p>
+     *
+     * @see Builder#setDimBrightness(float)
+     */
+    @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+    public @FloatRange(from = 0.0f, to = 1.0f) float getDimBrightness() {
+        return mDimBrightness;
+    }
+
+    /**
      * Returns the listener to get notified about changes in the display brightness.
      * @hide
      */
@@ -278,6 +294,7 @@
         DisplayCutout.ParcelableWrapper.writeCutoutToParcel(mDisplayCutout, dest, flags);
         dest.writeBoolean(mIgnoreActivitySizeRestrictions);
         dest.writeFloat(mDefaultBrightness);
+        dest.writeFloat(mDimBrightness);
         dest.writeStrongBinder(mBrightnessListener != null ? mBrightnessListener.asBinder() : null);
     }
 
@@ -308,8 +325,8 @@
                 && mIgnoreActivitySizeRestrictions == that.mIgnoreActivitySizeRestrictions
                 && Objects.equals(mDisplayCutout, that.mDisplayCutout)
                 && mDefaultBrightness == that.mDefaultBrightness
+                && mDimBrightness == that.mDimBrightness
                 && Objects.equals(mBrightnessListener, that.mBrightnessListener);
-
     }
 
     @Override
@@ -318,7 +335,8 @@
                 mName, mWidth, mHeight, mDensityDpi, mFlags, mSurface, mUniqueId,
                 mDisplayIdToMirror, mWindowManagerMirroringEnabled, mDisplayCategories,
                 mRequestedRefreshRate, mIsHomeSupported, mDisplayCutout,
-                mIgnoreActivitySizeRestrictions, mDefaultBrightness, mBrightnessListener);
+                mIgnoreActivitySizeRestrictions, mDefaultBrightness, mDimBrightness,
+                mBrightnessListener);
         return hashCode;
     }
 
@@ -341,6 +359,7 @@
                 + " mDisplayCutout=" + mDisplayCutout
                 + " mIgnoreActivitySizeRestrictions=" + mIgnoreActivitySizeRestrictions
                 + " mDefaultBrightness=" + mDefaultBrightness
+                + " mDimBrightness=" + mDimBrightness
                 + ")";
     }
 
@@ -360,8 +379,8 @@
         mDisplayCutout = DisplayCutout.ParcelableWrapper.readCutoutFromParcel(in);
         mIgnoreActivitySizeRestrictions = in.readBoolean();
         mDefaultBrightness = in.readFloat();
+        mDimBrightness = in.readFloat();
         mBrightnessListener = IBrightnessListener.Stub.asInterface(in.readStrongBinder());
-
     }
 
     /**
@@ -432,6 +451,7 @@
         private DisplayCutout mDisplayCutout = null;
         private boolean mIgnoreActivitySizeRestrictions = false;
         private float mDefaultBrightness = 0.0f;
+        private float mDimBrightness = PowerManager.BRIGHTNESS_INVALID;
         private IBrightnessListener mBrightnessListener = null;
 
         /**
@@ -635,14 +655,14 @@
          *
          * <p>If unset, defaults to {@code 0.0}</p>
          *
+         * @throws IllegalArgumentException if the brightness is outside the valid range [0.0, 1.0]
          * @see android.view.View#setKeepScreenOn(boolean)
          * @see #setBrightnessListener(Executor, BrightnessListener)
          */
         @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
         @NonNull
         public Builder setDefaultBrightness(@FloatRange(from = 0.0f, to = 1.0f) float brightness) {
-            if (brightness < PowerManager.BRIGHTNESS_MIN
-                    || brightness > PowerManager.BRIGHTNESS_MAX) {
+            if (!isValidBrightness(brightness)) {
                 throw new IllegalArgumentException(
                         "Virtual display default brightness must be in range [0.0, 1.0]");
             }
@@ -651,6 +671,33 @@
         }
 
         /**
+         * Sets the dim brightness of the display.
+         *
+         * <p>The system will use this brightness value whenever the display should be dim, i.e.
+         * it is powered on and dimmed due to user activity or app activity.</p>
+         *
+         * <p>Value of {@code 0.0} indicates the minimum supported brightness and value of
+         * {@code 1.0} indicates the maximum supported brightness.</p>
+         *
+         * <p>If set, the default brightness must also be set to a value greater or equal to the
+         * dim brightness. If unset, defaults to the system default.</p>
+         *
+         * @throws IllegalArgumentException if the brightness is outside the valid range [0.0, 1.0]
+         * @see Builder#setDefaultBrightness(float)
+         * @see #setBrightnessListener(Executor, BrightnessListener)
+         */
+        @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+        @NonNull
+        public Builder setDimBrightness(@FloatRange(from = 0.0f, to = 1.0f) float brightness) {
+            if (!isValidBrightness(brightness)) {
+                throw new IllegalArgumentException(
+                        "Virtual display dim brightness must be in range [0.0, 1.0]");
+            }
+            mDimBrightness = brightness;
+            return this;
+        }
+
+        /**
          * Sets the listener to get notified about changes in the display brightness.
          *
          * @param executor The executor where the callback is executed on.
@@ -666,11 +713,23 @@
             return this;
         }
 
+        private boolean isValidBrightness(float brightness) {
+            return !Float.isNaN(brightness) && PowerManager.BRIGHTNESS_MIN <= brightness
+                    && brightness <= PowerManager.BRIGHTNESS_MAX;
+        }
+
         /**
          * Builds the {@link VirtualDisplayConfig} instance.
+         *
+         * @throws IllegalArgumentException if the dim brightness is set to a value greater than
+         *   the default brightness.
          */
         @NonNull
         public VirtualDisplayConfig build() {
+            if (isValidBrightness(mDimBrightness) && mDimBrightness > mDefaultBrightness) {
+                throw new IllegalArgumentException(
+                        "The dim brightness must not be greater than the default brightness");
+            }
             return new VirtualDisplayConfig(
                     mName,
                     mWidth,
@@ -687,6 +746,7 @@
                     mDisplayCutout,
                     mIgnoreActivitySizeRestrictions,
                     mDefaultBrightness,
+                    mDimBrightness,
                     mBrightnessListener);
         }
     }
diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl
index e85e580..4cac4dee 100644
--- a/core/java/android/os/IPowerManager.aidl
+++ b/core/java/android/os/IPowerManager.aidl
@@ -55,7 +55,7 @@
     void goToSleepWithDisplayId(int displayId, long time, int reason, int flags);
     @UnsupportedAppUsage(maxTargetSdk = 28)
     void nap(long time);
-    float getBrightnessConstraint(int constraint);
+    float getBrightnessConstraint(int displayId, int constraint);
     @UnsupportedAppUsage
     boolean isInteractive();
     boolean isDisplayInteractive(int displayId);
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 9e7bf47..cd48f08 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -1266,9 +1266,17 @@
      * @hide
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
-    public float getBrightnessConstraint(int constraint) {
+    public float getBrightnessConstraint(@BrightnessConstraint int constraint) {
+        return getBrightnessConstraint(Display.DEFAULT_DISPLAY, constraint);
+    }
+
+    /**
+     * Gets a float screen brightness setting for a specific display.
+     * @hide
+     */
+    public float getBrightnessConstraint(int displayId, @BrightnessConstraint int constraint) {
         try {
-            return mService.getBrightnessConstraint(constraint);
+            return mService.getBrightnessConstraint(displayId, constraint);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 8b6458a..4307884 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -361,6 +361,12 @@
     public float brightnessDefault;
 
     /**
+     * The current dim brightness of the display. Value between 0.0 and 1.0,
+     * derived from the configuration of the display device of this logical display.
+     */
+    public float brightnessDim;
+
+    /**
      * The {@link RoundedCorners} if present, otherwise {@code null}.
      */
     @Nullable
@@ -479,6 +485,7 @@
                 && brightnessMinimum == other.brightnessMinimum
                 && brightnessMaximum == other.brightnessMaximum
                 && brightnessDefault == other.brightnessDefault
+                && brightnessDim == other.brightnessDim
                 && Objects.equals(roundedCorners, other.roundedCorners)
                 && installOrientation == other.installOrientation
                 && Objects.equals(displayShape, other.displayShape)
@@ -546,6 +553,7 @@
         brightnessMinimum = other.brightnessMinimum;
         brightnessMaximum = other.brightnessMaximum;
         brightnessDefault = other.brightnessDefault;
+        brightnessDim = other.brightnessDim;
         roundedCorners = other.roundedCorners;
         installOrientation = other.installOrientation;
         displayShape = other.displayShape;
@@ -620,6 +628,7 @@
         brightnessMinimum = source.readFloat();
         brightnessMaximum = source.readFloat();
         brightnessDefault = source.readFloat();
+        brightnessDim = source.readFloat();
         roundedCorners = source.readTypedObject(RoundedCorners.CREATOR);
         int numUserDisabledFormats = source.readInt();
         userDisabledHdrTypes = new int[numUserDisabledFormats];
@@ -696,6 +705,7 @@
         dest.writeFloat(brightnessMinimum);
         dest.writeFloat(brightnessMaximum);
         dest.writeFloat(brightnessDefault);
+        dest.writeFloat(brightnessDim);
         dest.writeTypedObject(roundedCorners, flags);
         dest.writeInt(userDisabledHdrTypes.length);
         for (int i = 0; i < userDisabledHdrTypes.length; i++) {
@@ -994,6 +1004,8 @@
         sb.append(brightnessMaximum);
         sb.append(", brightnessDefault ");
         sb.append(brightnessDefault);
+        sb.append(", brightnessDim ");
+        sb.append(brightnessDim);
         sb.append(", installOrientation ");
         sb.append(Surface.rotationToString(installOrientation));
         sb.append(", layoutLimitedRefreshRate ");
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 02c7901..049189f 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -28277,7 +28277,7 @@
         mPrivateFlags |= PFLAG_FORCE_LAYOUT;
         mPrivateFlags |= PFLAG_INVALIDATED;
 
-        if (mParent != null) {
+        if (mParent != null && !mParent.isLayoutRequested()) {
             mParent.requestLayout();
         }
         if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 81734a3..d0d4af6 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -116,3 +116,14 @@
     description: "Relax the assumption of non-match parent activity"
     bug: "356277166"
 }
+
+flag {
+    namespace: "windowing_sdk"
+    name: "allow_multiple_adjacent_task_fragments"
+    description: "Refactor to allow more than 2 adjacent TaskFragments"
+    bug: "373709676"
+    is_fixed_read_only: true
+    metadata {
+        purpose: PURPOSE_BUGFIX
+    }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java
new file mode 100644
index 0000000..a090c7a
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java
@@ -0,0 +1,184 @@
+/*
+ * 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.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_DURATION_MS;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INITIAL_SHARPNESS;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INTENSITY;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_SHARPNESS;
+import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_CONTROL_POINT;
+
+import android.annotation.NonNull;
+import android.os.VibrationEffect;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Serialized representation of a basic envelope effect created via
+ * {@link VibrationEffect.BasicEnvelopeBuilder}.
+ *
+ * @hide
+ */
+final class SerializedBasicEnvelopeEffect implements SerializedComposedEffect.SerializedSegment {
+    private final BasicControlPoint[] mControlPoints;
+    private final float mInitialSharpness;
+
+    SerializedBasicEnvelopeEffect(BasicControlPoint[] controlPoints, float initialSharpness) {
+        mControlPoints = controlPoints;
+        mInitialSharpness = initialSharpness;
+    }
+
+    @Override
+    public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+        serializer.startTag(NAMESPACE, TAG_BASIC_ENVELOPE_EFFECT);
+
+        if (!Float.isNaN(mInitialSharpness)) {
+            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INITIAL_SHARPNESS, mInitialSharpness);
+        }
+
+        for (BasicControlPoint point : mControlPoints) {
+            serializer.startTag(NAMESPACE, TAG_CONTROL_POINT);
+            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INTENSITY, point.mIntensity);
+            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_SHARPNESS, point.mSharpness);
+            serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, point.mDurationMs);
+            serializer.endTag(NAMESPACE, TAG_CONTROL_POINT);
+        }
+
+        serializer.endTag(NAMESPACE, TAG_BASIC_ENVELOPE_EFFECT);
+    }
+
+    @Override
+    public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+        VibrationEffect.BasicEnvelopeBuilder builder = new VibrationEffect.BasicEnvelopeBuilder();
+
+        if (!Float.isNaN(mInitialSharpness)) {
+            builder.setInitialSharpness(mInitialSharpness);
+        }
+
+        for (BasicControlPoint point : mControlPoints) {
+            builder.addControlPoint(point.mIntensity, point.mSharpness, point.mDurationMs);
+        }
+        composition.addEffect(builder.build());
+    }
+
+    @Override
+    public String toString() {
+        return "SerializedBasicEnvelopeEffect{"
+                + "initialSharpness=" + (Float.isNaN(mInitialSharpness) ? "" : mInitialSharpness)
+                + ", controlPoints=" + Arrays.toString(mControlPoints)
+                + '}';
+    }
+
+    static final class Builder {
+        private final List<BasicControlPoint> mControlPoints;
+        private float mInitialSharpness = Float.NaN;
+
+        Builder() {
+            mControlPoints = new ArrayList<>();
+        }
+
+        void setInitialSharpness(float sharpness) {
+            mInitialSharpness = sharpness;
+        }
+
+        void addControlPoint(float intensity, float sharpness, long durationMs) {
+            mControlPoints.add(new BasicControlPoint(intensity, sharpness, durationMs));
+        }
+
+        SerializedBasicEnvelopeEffect build() {
+            return new SerializedBasicEnvelopeEffect(
+                    mControlPoints.toArray(new BasicControlPoint[0]), mInitialSharpness);
+        }
+    }
+
+    /** Parser implementation for {@link SerializedBasicEnvelopeEffect}. */
+    static final class Parser {
+
+        @NonNull
+        static SerializedBasicEnvelopeEffect parseNext(@NonNull TypedXmlPullParser parser,
+                @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+            XmlValidator.checkStartTag(parser, TAG_BASIC_ENVELOPE_EFFECT);
+            XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_INITIAL_SHARPNESS);
+
+            Builder builder = new Builder();
+            builder.setInitialSharpness(
+                    XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_INITIAL_SHARPNESS, 0f, 1f,
+                            Float.NaN));
+
+            int outerDepth = parser.getDepth();
+
+            // Read all nested tags
+            while (XmlReader.readNextTagWithin(parser, outerDepth)) {
+                parseControlPoint(parser, builder);
+                // Consume tag
+                XmlReader.readEndTag(parser);
+            }
+
+            // Check schema assertions about <basic-envelope-effect>
+            XmlValidator.checkParserCondition(!builder.mControlPoints.isEmpty(),
+                    "Expected tag %s to have at least one control point",
+                    TAG_BASIC_ENVELOPE_EFFECT);
+            XmlValidator.checkParserCondition(builder.mControlPoints.getLast().mIntensity == 0,
+                    "Basic envelope effects must end at a zero intensity control point");
+
+            return builder.build();
+        }
+
+        private static void parseControlPoint(TypedXmlPullParser parser, Builder builder)
+                throws XmlParserException {
+            XmlValidator.checkStartTag(parser, TAG_CONTROL_POINT);
+            XmlValidator.checkTagHasNoUnexpectedAttributes(
+                    parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_INTENSITY,
+                    ATTRIBUTE_SHARPNESS);
+            float intensity = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_INTENSITY, 0,
+                    1);
+            float sharpness = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_SHARPNESS, 0,
+                    1);
+            long durationMs = XmlReader.readAttributePositiveLong(parser, ATTRIBUTE_DURATION_MS);
+
+            builder.addControlPoint(intensity, sharpness, durationMs);
+        }
+    }
+
+    private static final class BasicControlPoint {
+        private final float mIntensity;
+        private final float mSharpness;
+        private final long mDurationMs;
+
+        BasicControlPoint(float intensity, float sharpness, long durationMs) {
+            mIntensity = intensity;
+            mSharpness = sharpness;
+            mDurationMs = durationMs;
+        }
+
+        @Override
+        public String toString() {
+            return String.format(Locale.ROOT, "(%.2f, %.2f, %dms)", mIntensity, mSharpness,
+                    mDurationMs);
+        }
+    }
+}
+
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java
new file mode 100644
index 0000000..6a89343
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java
@@ -0,0 +1,182 @@
+/*
+ * 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.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_AMPLITUDE;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_DURATION_MS;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_FREQUENCY_HZ;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INITIAL_FREQUENCY_HZ;
+import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_CONTROL_POINT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;
+
+import android.annotation.NonNull;
+import android.os.VibrationEffect;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Serialized representation of a waveform envelope effect created via
+ * {@link VibrationEffect.WaveformEnvelopeBuilder}.
+ *
+ * @hide
+ */
+final class SerializedWaveformEnvelopeEffect implements SerializedComposedEffect.SerializedSegment {
+
+    private final WaveformControlPoint[] mControlPoints;
+    private final float mInitialFrequency;
+
+    SerializedWaveformEnvelopeEffect(WaveformControlPoint[] controlPoints, float initialFrequency) {
+        mControlPoints = controlPoints;
+        mInitialFrequency = initialFrequency;
+    }
+
+    @Override
+    public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+        serializer.startTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT);
+
+        if (!Float.isNaN(mInitialFrequency)) {
+            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INITIAL_FREQUENCY_HZ, mInitialFrequency);
+        }
+
+        for (WaveformControlPoint point : mControlPoints) {
+            serializer.startTag(NAMESPACE, TAG_CONTROL_POINT);
+            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_AMPLITUDE, point.mAmplitude);
+            serializer.attributeFloat(NAMESPACE, ATTRIBUTE_FREQUENCY_HZ, point.mFrequency);
+            serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, point.mDurationMs);
+            serializer.endTag(NAMESPACE, TAG_CONTROL_POINT);
+        }
+
+        serializer.endTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT);
+    }
+
+    @Override
+    public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+        VibrationEffect.WaveformEnvelopeBuilder builder =
+                new VibrationEffect.WaveformEnvelopeBuilder();
+
+        if (!Float.isNaN(mInitialFrequency)) {
+            builder.setInitialFrequencyHz(mInitialFrequency);
+        }
+
+        for (WaveformControlPoint point : mControlPoints) {
+            builder.addControlPoint(point.mAmplitude, point.mFrequency, point.mDurationMs);
+        }
+        composition.addEffect(builder.build());
+    }
+
+    @Override
+    public String toString() {
+        return "SerializedWaveformEnvelopeEffect{"
+                + "InitialFrequency=" + (Float.isNaN(mInitialFrequency) ? "" : mInitialFrequency)
+                + ", controlPoints=" + Arrays.toString(mControlPoints)
+                + '}';
+    }
+
+    static final class Builder {
+        private final List<WaveformControlPoint> mControlPoints;
+        private float mInitialFrequencyHz = Float.NaN;
+
+        Builder() {
+            mControlPoints = new ArrayList<>();
+        }
+
+        void setInitialFrequencyHz(float frequencyHz) {
+            mInitialFrequencyHz = frequencyHz;
+        }
+
+        void addControlPoint(float amplitude, float frequencyHz, long durationMs) {
+            mControlPoints.add(new WaveformControlPoint(amplitude, frequencyHz, durationMs));
+        }
+
+        SerializedWaveformEnvelopeEffect build() {
+            return new SerializedWaveformEnvelopeEffect(
+                    mControlPoints.toArray(new WaveformControlPoint[0]), mInitialFrequencyHz);
+        }
+    }
+
+    /** Parser implementation for {@link SerializedWaveformEnvelopeEffect}. */
+    static final class Parser {
+
+        @NonNull
+        static SerializedWaveformEnvelopeEffect parseNext(@NonNull TypedXmlPullParser parser,
+                @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+            XmlValidator.checkStartTag(parser, TAG_WAVEFORM_ENVELOPE_EFFECT);
+            XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ);
+
+            Builder builder = new Builder();
+            builder.setInitialFrequencyHz(
+                    XmlReader.readAttributePositiveFloat(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ,
+                            Float.NaN));
+
+            int outerDepth = parser.getDepth();
+
+            while (XmlReader.readNextTagWithin(parser, outerDepth)) {
+                parseControlPoint(parser, builder);
+                // Consume tag
+                XmlReader.readEndTag(parser);
+            }
+
+            // Check schema assertions about <waveform-envelope-effect>
+            XmlValidator.checkParserCondition(!builder.mControlPoints.isEmpty(),
+                    "Expected tag %s to have at least one control point",
+                    TAG_WAVEFORM_ENVELOPE_EFFECT);
+
+            return builder.build();
+        }
+
+        private static void parseControlPoint(TypedXmlPullParser parser, Builder builder)
+                throws XmlParserException {
+            XmlValidator.checkStartTag(parser, TAG_CONTROL_POINT);
+            XmlValidator.checkTagHasNoUnexpectedAttributes(
+                    parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_AMPLITUDE,
+                    ATTRIBUTE_FREQUENCY_HZ);
+            float amplitude = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_AMPLITUDE, 0,
+                    1);
+            float frequencyHz = XmlReader.readAttributePositiveFloat(parser,
+                    ATTRIBUTE_FREQUENCY_HZ);
+            long durationMs = XmlReader.readAttributePositiveLong(parser, ATTRIBUTE_DURATION_MS);
+
+            builder.addControlPoint(amplitude, frequencyHz, durationMs);
+        }
+    }
+
+    private static final class WaveformControlPoint {
+        private final float mAmplitude;
+        private final float mFrequency;
+        private final long mDurationMs;
+
+        WaveformControlPoint(float amplitude, float frequency, long durationMs) {
+            mAmplitude = amplitude;
+            mFrequency = frequency;
+            mDurationMs = durationMs;
+        }
+
+        @Override
+        public String toString() {
+            return String.format(Locale.ROOT, "(%.2f, %.2f, %dms)", mAmplitude, mFrequency,
+                    mDurationMs);
+        }
+    }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
index a9fbcaf..314bfe4 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
@@ -16,11 +16,13 @@
 
 package com.android.internal.vibrator.persistence;
 
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PRIMITIVE_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VIBRATION_EFFECT;
 import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;
 
 import android.annotation.NonNull;
 import android.os.VibrationEffect;
@@ -92,6 +94,32 @@
  *   }
  * </pre>
  *
+ * * Waveform Envelope effects
+ *
+ * <pre>
+ *     {@code
+ *       <vibration-effect>
+ *         <waveform-envelope-effect initialFrequencyHz="20.0">
+ *           <control-point amplitude="0.2" frequencyHz="80.0" durationMs="50" />
+ *           <control-point amplitude="0.5" frequencyHz="150.0" durationMs="50" />
+ *         </envelope-effect>
+ *       </vibration-effect>
+ *     }
+ * </pre>
+ *
+ * * Basic Envelope effects
+ *
+ * <pre>
+ *     {@code
+ *       <vibration-effect>
+ *         <basic-envelope-effect initialSharpness="0.3">
+ *            <control-point intensity="0.2" sharpness="0.5" durationMs="50" />
+ *            <control-point intensity="0.0" sharpness="1.0" durationMs="50" />
+ *          </envelope-effect>
+ *       </vibration-effect>
+ *     }
+ * </pre>
+ *
  * @hide
  */
 public class VibrationEffectXmlParser {
@@ -151,6 +179,18 @@
                 serializedVibration = new SerializedComposedEffect(
                         SerializedAmplitudeStepWaveform.Parser.parseNext(parser));
                 break;
+            case TAG_WAVEFORM_ENVELOPE_EFFECT:
+                if (Flags.normalizedPwleEffects()) {
+                    serializedVibration = new SerializedComposedEffect(
+                            SerializedWaveformEnvelopeEffect.Parser.parseNext(parser, flags));
+                    break;
+                } // else fall through
+            case TAG_BASIC_ENVELOPE_EFFECT:
+                if (Flags.normalizedPwleEffects()) {
+                    serializedVibration = new SerializedComposedEffect(
+                            SerializedBasicEnvelopeEffect.Parser.parseNext(parser, flags));
+                    break;
+                } // else fall through
             default:
                 throw new XmlParserException("Unexpected tag " + parser.getName()
                         + " in vibration tag " + vibrationTagName);
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
index cb834a5..ebe3434 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
@@ -19,9 +19,11 @@
 import android.annotation.NonNull;
 import android.os.PersistableBundle;
 import android.os.VibrationEffect;
+import android.os.vibrator.BasicPwleSegment;
 import android.os.vibrator.Flags;
 import android.os.vibrator.PrebakedSegment;
 import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
 import android.os.vibrator.StepSegment;
 import android.os.vibrator.VibrationEffectSegment;
 
@@ -45,6 +47,8 @@
  *     <li>A composition created exclusively via
  *         {@link VibrationEffect.Composition#addPrimitive(int, float, int)}
  *     <li>{@link VibrationEffect#createVendorEffect(PersistableBundle)}
+ *     <li>{@link VibrationEffect.WaveformEnvelopeBuilder}
+ *     <li>{@link VibrationEffect.BasicEnvelopeBuilder}
  * </ul>
  *
  * @hide
@@ -77,6 +81,12 @@
         if (firstSegment instanceof PrimitiveSegment) {
             return serializePrimitiveEffect(composed);
         }
+        if (Flags.normalizedPwleEffects() && firstSegment instanceof PwleSegment) {
+            return serializeWaveformEnvelopeEffect(composed);
+        }
+        if (Flags.normalizedPwleEffects() && firstSegment instanceof BasicPwleSegment) {
+            return serializeBasicEnvelopeEffect(composed);
+        }
         return serializeWaveformEffect(composed);
     }
 
@@ -110,6 +120,53 @@
         return new SerializedComposedEffect(primitives);
     }
 
+    private static SerializedComposedEffect serializeWaveformEnvelopeEffect(
+            VibrationEffect.Composed effect) throws XmlSerializerException {
+        SerializedWaveformEnvelopeEffect.Builder builder =
+                new SerializedWaveformEnvelopeEffect.Builder();
+        List<VibrationEffectSegment> segments = effect.getSegments();
+        XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
+                "Unsupported repeating waveform envelope effect %s", effect);
+        for (int i = 0; i < segments.size(); i++) {
+            XmlValidator.checkSerializerCondition(segments.get(i) instanceof PwleSegment,
+                    "Unsupported segment for waveform envelope effect %s", segments.get(i));
+            PwleSegment segment = (PwleSegment) segments.get(i);
+
+            if (i == 0 && segment.getStartFrequencyHz() != segment.getEndFrequencyHz()) {
+                // Initial frequency explicitly defined.
+                builder.setInitialFrequencyHz(segment.getStartFrequencyHz());
+            }
+
+            builder.addControlPoint(segment.getEndAmplitude(), segment.getEndFrequencyHz(),
+                    segment.getDuration());
+        }
+
+        return new SerializedComposedEffect(builder.build());
+    }
+
+    private static SerializedComposedEffect serializeBasicEnvelopeEffect(
+            VibrationEffect.Composed effect) throws XmlSerializerException {
+        SerializedBasicEnvelopeEffect.Builder builder = new SerializedBasicEnvelopeEffect.Builder();
+        List<VibrationEffectSegment> segments = effect.getSegments();
+        XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
+                "Unsupported repeating basic envelope effect %s", effect);
+        for (int i = 0; i < segments.size(); i++) {
+            XmlValidator.checkSerializerCondition(segments.get(i) instanceof BasicPwleSegment,
+                    "Unsupported segment for basic envelope effect %s", segments.get(i));
+            BasicPwleSegment segment = (BasicPwleSegment) segments.get(i);
+
+            if (i == 0 && segment.getStartSharpness() != segment.getEndSharpness()) {
+                // Initial sharpness explicitly defined.
+                builder.setInitialSharpness(segment.getStartSharpness());
+            }
+
+            builder.addControlPoint(segment.getEndIntensity(), segment.getEndSharpness(),
+                    segment.getDuration());
+        }
+
+        return new SerializedComposedEffect(builder.build());
+    }
+
     private static SerializedComposedEffect serializeWaveformEffect(
             VibrationEffect.Composed effect) throws XmlSerializerException {
         SerializedAmplitudeStepWaveform.Builder serializedWaveformBuilder =
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
index 4122215..df262cf 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
@@ -42,14 +42,22 @@
     public static final String TAG_PREDEFINED_EFFECT = "predefined-effect";
     public static final String TAG_PRIMITIVE_EFFECT = "primitive-effect";
     public static final String TAG_VENDOR_EFFECT = "vendor-effect";
+    public static final String TAG_WAVEFORM_ENVELOPE_EFFECT = "waveform-envelope-effect";
+    public static final String TAG_BASIC_ENVELOPE_EFFECT = "basic-envelope-effect";
     public static final String TAG_WAVEFORM_EFFECT = "waveform-effect";
     public static final String TAG_WAVEFORM_ENTRY = "waveform-entry";
     public static final String TAG_REPEATING = "repeating";
+    public static final String TAG_CONTROL_POINT = "control-point";
 
     public static final String ATTRIBUTE_NAME = "name";
     public static final String ATTRIBUTE_FALLBACK = "fallback";
     public static final String ATTRIBUTE_DURATION_MS = "durationMs";
     public static final String ATTRIBUTE_AMPLITUDE = "amplitude";
+    public static final String ATTRIBUTE_FREQUENCY_HZ = "frequencyHz";
+    public static final String ATTRIBUTE_INITIAL_FREQUENCY_HZ = "initialFrequencyHz";
+    public static final String ATTRIBUTE_INTENSITY = "intensity";
+    public static final String ATTRIBUTE_SHARPNESS = "sharpness";
+    public static final String ATTRIBUTE_INITIAL_SHARPNESS = "initialSharpness";
     public static final String ATTRIBUTE_SCALE = "scale";
     public static final String ATTRIBUTE_DELAY_MS = "delayMs";
     public static final String ATTRIBUTE_DELAY_TYPE = "delayType";
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlReader.java b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
index 0ac6fef..1c4a783 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlReader.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
@@ -221,12 +221,63 @@
         if (parser.getAttributeIndex(NAMESPACE, attrName) < 0) {
             return defaultValue;
         }
+
+        return readAttributeFloatInRange(parser, attrName, lowerInclusive, upperInclusive);
+    }
+
+    /**
+     * Read attribute from current tag as a float within given inclusive range.
+     */
+    public static float readAttributeFloatInRange(
+            TypedXmlPullParser parser, String attrName, float lowerInclusive,
+            float upperInclusive) throws XmlParserException {
         String tagName = parser.getName();
         float value = readAttributeFloat(parser, attrName);
 
         XmlValidator.checkParserCondition(value >= lowerInclusive && value <= upperInclusive,
-                "Unexpected %s = %f in tag %s, expected %s in [%f, %f]",
-                attrName, value, tagName, attrName, lowerInclusive, upperInclusive);
+                "Unexpected %s = %f in tag %s, expected %s in [%f, %f]", attrName, value, tagName,
+                attrName, lowerInclusive, upperInclusive);
+        return value;
+    }
+
+    /**
+     * Read attribute from current tag as a positive float, returning default value if attribute
+     * is missing.
+     */
+    public static float readAttributePositiveFloat(TypedXmlPullParser parser, String attrName,
+            float defaultValue) throws XmlParserException {
+        if (parser.getAttributeIndex(NAMESPACE, attrName) < 0) {
+            return defaultValue;
+        }
+
+        return readAttributePositiveFloat(parser, attrName);
+    }
+
+    /**
+     * Read attribute from current tag as a positive float.
+     */
+    public static float readAttributePositiveFloat(TypedXmlPullParser parser, String attrName)
+            throws XmlParserException {
+        String tagName = parser.getName();
+        float value = readAttributeFloat(parser, attrName);
+
+        XmlValidator.checkParserCondition(value > 0,
+                "Unexpected %s = %d in tag %s, expected %s > 0", attrName, value, tagName,
+                attrName);
+        return value;
+    }
+
+    /**
+     * Read attribute from current tag as a positive long.
+     */
+    public static long readAttributePositiveLong(TypedXmlPullParser parser, String attrName)
+            throws XmlParserException {
+        String tagName = parser.getName();
+        long value = readAttributeLong(parser, attrName);
+
+        XmlValidator.checkParserCondition(value > 0,
+                "Unexpected %s = %d in tag %s, expected %s > 0", attrName, value, tagName,
+                attrName);
         return value;
     }
 
@@ -251,4 +302,15 @@
             throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e);
         }
     }
+
+    private static long readAttributeLong(TypedXmlPullParser parser, String attrName)
+            throws XmlParserException {
+        String tagName = parser.getName();
+        try {
+            return parser.getAttributeLong(NAMESPACE, attrName);
+        } catch (XmlPullParserException e) {
+            String rawValue = parser.getAttributeValue(NAMESPACE, attrName);
+            throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e);
+        }
+    }
 }
diff --git a/core/tests/coretests/src/android/view/ViewGroupTest.java b/core/tests/coretests/src/android/view/ViewGroupTest.java
index 43c404e..ae3ad36 100644
--- a/core/tests/coretests/src/android/view/ViewGroupTest.java
+++ b/core/tests/coretests/src/android/view/ViewGroupTest.java
@@ -213,35 +213,6 @@
         assertTrue(autofillableViews.containsAll(Arrays.asList(viewA, viewC)));
     }
 
-    @Test
-    public void testMeasureCache() {
-        final int spec1 = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST);
-        final int spec2 = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.AT_MOST);
-        final Context context = getInstrumentation().getContext();
-        final View child = new View(context);
-        final TestView parent = new TestView(context, 0);
-        parent.addView(child);
-
-        child.setPadding(1, 2, 3, 4);
-        parent.measure(spec1, spec1);
-        assertEquals(4, parent.getMeasuredWidth());
-        assertEquals(6, parent.getMeasuredHeight());
-
-        child.setPadding(5, 6, 7, 8);
-        parent.measure(spec2, spec2);
-        assertEquals(12, parent.getMeasuredWidth());
-        assertEquals(14, parent.getMeasuredHeight());
-
-        // This ends the state of forceLayout.
-        parent.layout(0, 0, 50, 50);
-
-        // The cached values should be cleared after the new setPadding is called. And the measured
-        // width and height should be up-to-date.
-        parent.measure(spec1, spec1);
-        assertEquals(12, parent.getMeasuredWidth());
-        assertEquals(14, parent.getMeasuredHeight());
-    }
-
     private static void getUnobscuredTouchableRegion(Region outRegion, View view) {
         outRegion.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
         final ViewParent parent = view.getParent();
@@ -269,19 +240,6 @@
         protected void onLayout(boolean changed, int l, int t, int r, int b) {
             // We don't layout this view.
         }
-
-        @Override
-        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-            int measuredWidth = 0;
-            int measuredHeight = 0;
-            final int count = getChildCount();
-            for (int i = 0; i < count; i++) {
-                final View child = getChildAt(i);
-                measuredWidth += child.getPaddingLeft() + child.getPaddingRight();
-                measuredHeight += child.getPaddingTop() + child.getPaddingBottom();
-            }
-            setMeasuredDimension(measuredWidth, measuredHeight);
-        }
     }
 
     public static class AutofillableTestView extends TestView {
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index 4071057..5f25e93 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -439,6 +439,498 @@
     }
 
     @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testWaveformEnvelopeEffect_allSucceed() throws Exception {
+        VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
+                .addControlPoint(0.2f, 80f, 10)
+                .addControlPoint(0.5f, 150f, 10)
+                .build();
+
+        String xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect>
+                        <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+                        <control-point amplitude="0.5" frequencyHz="150.0" durationMs="10" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, xml);
+        assertPublicApisRoundTrip(effect);
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, xml);
+        assertHiddenApisRoundTrip(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testWaveformEnvelopeEffectWithInitialFrequency_allSucceed() throws Exception {
+        VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
+                .setInitialFrequencyHz(20)
+                .addControlPoint(0.2f, 80f, 10)
+                .addControlPoint(0.5f, 150f, 10)
+                .build();
+
+        String xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect initialFrequencyHz="20.0">
+                        <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+                        <control-point amplitude="0.5" frequencyHz="150.0" durationMs="10" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, xml);
+        assertPublicApisRoundTrip(effect);
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, xml);
+        assertHiddenApisRoundTrip(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testWaveformEnvelopeEffect_badXml_throwsException() throws IOException {
+        // Incomplete XML
+        assertParseElementFails("""
+                <vibration-effect>
+                    <waveform-envelope-effect>
+                        <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <vibration-effect>
+                    <waveform-envelope-effect>
+                        <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10">
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <vibration-effect>
+                    <waveform-envelope-effect>
+                        <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+                    </waveform-envelope-effect>
+                """);
+
+        // Bad vibration XML
+        assertParseElementFails("""
+                <vibration-effect>
+                        <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """);
+
+        // "waveform-envelope-effect" tag with invalid attributes
+        assertParseElementFails("""
+                <vibration-effect>
+                    <waveform-envelope-effect init_freq="20.0">
+                        <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testWaveformEnvelopeEffect_noControlPoints_allFail() throws IOException {
+        String xml = "<vibration-effect><waveform-envelope-effect/></vibration-effect>";
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = "<vibration-effect><waveform-envelope-effect> \n "
+                + "</waveform-envelope-effect></vibration-effect>";
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = "<vibration-effect><waveform-envelope-effect>invalid</waveform-envelope-effect"
+                + "></vibration-effect>";
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect>
+                    <control-point />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect initialFrequencyHz="20.0" />
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                <waveform-envelope-effect initialFrequencyHz="20.0"> \n </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect initialFrequencyHz="20.0">
+                    invalid
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect initialFrequencyHz="20.0">
+                    <control-point />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testWaveformEnvelopeEffect_badControlPointData_allFail() throws IOException {
+        String xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect>
+                    <control-point amplitude="-1" frequencyHz="80.0" durationMs="100" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect>
+                    <control-point amplitude="0.2" frequencyHz="0" durationMs="100" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect initialFrequencyHz="0">
+                    <control-point amplitude="0.2" frequencyHz="30" durationMs="100" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect>
+                    <control-point amplitude="0.2" frequencyHz="80.0" durationMs="0" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect>
+                    <control-point amplitude="0.2" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testWaveformEnvelopeEffect_featureFlagDisabled_allFail() throws Exception {
+        VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
+                .setInitialFrequencyHz(20)
+                .addControlPoint(0.2f, 80f, 10)
+                .addControlPoint(0.5f, 150f, 10)
+                .build();
+
+        String xml = """
+                <vibration-effect>
+                    <waveform-envelope-effect initialFrequencyHz="20.0">
+                        <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+                        <control-point amplitude="0.5" frequencyHz="150.0" durationMs="10" />
+                    </waveform-envelope-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserFails(xml);
+        assertPublicApisSerializerFails(effect);
+        assertHiddenApisParserFails(xml);
+        assertHiddenApisSerializerFails(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicEnvelopeEffect_allSucceed() throws Exception {
+        VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+                .addControlPoint(0.2f, 0.5f, 10)
+                .addControlPoint(0.0f, 1f, 10)
+                .build();
+
+        String xml = """
+                <vibration-effect>
+                    <basic-envelope-effect>
+                        <control-point intensity="0.2" sharpness="0.5" durationMs="10" />
+                        <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, xml);
+        assertPublicApisRoundTrip(effect);
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, xml);
+        assertHiddenApisRoundTrip(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicEnvelopeEffectWithInitialSharpness_allSucceed() throws Exception {
+        VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(0.3f)
+                .addControlPoint(0.2f, 0.5f, 10)
+                .addControlPoint(0.0f, 1f, 10)
+                .build();
+
+        String xml = """
+                <vibration-effect>
+                    <basic-envelope-effect initialSharpness="0.3">
+                        <control-point intensity="0.2" sharpness="0.5" durationMs="10" />
+                        <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserSucceeds(xml, effect);
+        assertPublicApisSerializerSucceeds(effect, xml);
+        assertPublicApisRoundTrip(effect);
+        assertHiddenApisParserSucceeds(xml, effect);
+        assertHiddenApisSerializerSucceeds(effect, xml);
+        assertHiddenApisRoundTrip(effect);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicEnvelopeEffect_badXml_throwsException() throws IOException {
+        // Incomplete XML
+        assertParseElementFails("""
+                <vibration-effect>
+                    <basic-envelope-effect>
+                        <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+                        <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <vibration-effect>
+                    <basic-envelope-effect>
+                        <control-point intensity="0.2" sharpness="0.8" durationMs="10">
+                        <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """);
+        assertParseElementFails("""
+                <vibration-effect>
+                    <basic-envelope-effect>
+                        <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+                        <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                """);
+
+        // Bad vibration XML
+        assertParseElementFails("""
+                <vibration-effect>
+                        <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+                        <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """);
+
+        // "basic-envelope-effect" tag with invalid attributes
+        assertParseElementFails("""
+                <vibration-effect>
+                    <basic-envelope-effect init_sharp="20.0">
+                        <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+                        <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicEnvelopeEffect_noControlPoints_allFail() throws IOException {
+        String xml = "<vibration-effect><basic-envelope-effect/></vibration-effect>";
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = "<vibration-effect><basic-envelope-effect> \n "
+                + "</basic-envelope-effect></vibration-effect>";
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = "<vibration-effect><basic-envelope-effect>invalid</basic-envelope-effect"
+                + "></vibration-effect>";
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <basic-envelope-effect>
+                    <control-point />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <basic-envelope-effect initialSharpness="0.2" />
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                <basic-envelope-effect initialSharpness="0.2"> \n </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <basic-envelope-effect initialSharpness="0.2">
+                    invalid
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <basic-envelope-effect initialSharpness="0.2">
+                    <control-point />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+    }
+
+    @Test
+    @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicEnvelopeEffect_badControlPointData_allFail() throws IOException {
+        String xml = """
+                <vibration-effect>
+                    <basic-envelope-effect>
+                    <control-point intensity="-1" sharpness="0.8" durationMs="100" />
+                    <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <basic-envelope-effect>
+                    <control-point intensity="0.2" sharpness="-1" durationMs="100" />
+                    <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <basic-envelope-effect initialSharpness="-1.0">
+                    <control-point intensity="0.2" sharpness="0.8" durationMs="0" />
+                    <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <basic-envelope-effect initialSharpness="2.0">
+                    <control-point intensity="0.2" sharpness="0.8" durationMs="0" />
+                    <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <basic-envelope-effect>
+                    <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+                    <control-point intensity="0.5" sharpness="0.8" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+
+        xml = """
+                <vibration-effect>
+                    <basic-envelope-effect>
+                    <control-point intensity="0.2" />
+                    <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+        assertPublicApisParserFails(xml);
+        assertHiddenApisParserFails(xml);
+    }
+
+    @Test
+    @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+    public void testBasicEnvelopeEffect_featureFlagDisabled_allFail() throws Exception {
+        VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+                .setInitialSharpness(0.3f)
+                .addControlPoint(0.2f, 0.5f, 10)
+                .addControlPoint(0.0f, 1f, 10)
+                .build();
+
+        String xml = """
+                <vibration-effect>
+                    <basic-envelope-effect initialSharpness="0.3">
+                        <control-point intensity="0.2" sharpness="0.5" durationMs="10" />
+                        <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+                    </basic-envelope-effect>
+                </vibration-effect>
+                """;
+
+        assertPublicApisParserFails(xml);
+        assertPublicApisSerializerFails(effect);
+
+        assertHiddenApisParserFails(xml);
+        assertHiddenApisSerializerFails(effect);
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
     public void testVendorEffect_allSucceed() throws Exception {
         PersistableBundle vendorData = new PersistableBundle();
diff --git a/core/xsd/vibrator/vibration/schema/current.txt b/core/xsd/vibrator/vibration/schema/current.txt
index b4148d6..29f8d19 100644
--- a/core/xsd/vibrator/vibration/schema/current.txt
+++ b/core/xsd/vibrator/vibration/schema/current.txt
@@ -1,6 +1,23 @@
 // Signature format: 2.0
 package com.android.internal.vibrator.persistence {
 
+  public class BasicControlPoint {
+    ctor public BasicControlPoint();
+    method public long getDurationMs();
+    method public float getIntensity();
+    method public float getSharpness();
+    method public void setDurationMs(long);
+    method public void setIntensity(float);
+    method public void setSharpness(float);
+  }
+
+  public class BasicEnvelopeEffect {
+    ctor public BasicEnvelopeEffect();
+    method public java.util.List<com.android.internal.vibrator.persistence.BasicControlPoint> getControlPoint();
+    method public float getInitialSharpness();
+    method public void setInitialSharpness(float);
+  }
+
   public class PredefinedEffect {
     ctor public PredefinedEffect();
     method public com.android.internal.vibrator.persistence.PredefinedEffectName getName();
@@ -47,14 +64,18 @@
 
   public class VibrationEffect {
     ctor public VibrationEffect();
+    method public com.android.internal.vibrator.persistence.BasicEnvelopeEffect getBasicEnvelopeEffect_optional();
     method public com.android.internal.vibrator.persistence.PredefinedEffect getPredefinedEffect_optional();
     method public com.android.internal.vibrator.persistence.PrimitiveEffect getPrimitiveEffect_optional();
     method public byte[] getVendorEffect_optional();
     method public com.android.internal.vibrator.persistence.WaveformEffect getWaveformEffect_optional();
+    method public com.android.internal.vibrator.persistence.WaveformEnvelopeEffect getWaveformEnvelopeEffect_optional();
+    method public void setBasicEnvelopeEffect_optional(com.android.internal.vibrator.persistence.BasicEnvelopeEffect);
     method public void setPredefinedEffect_optional(com.android.internal.vibrator.persistence.PredefinedEffect);
     method public void setPrimitiveEffect_optional(com.android.internal.vibrator.persistence.PrimitiveEffect);
     method public void setVendorEffect_optional(byte[]);
     method public void setWaveformEffect_optional(com.android.internal.vibrator.persistence.WaveformEffect);
+    method public void setWaveformEnvelopeEffect_optional(com.android.internal.vibrator.persistence.WaveformEnvelopeEffect);
   }
 
   public class VibrationSelect {
@@ -67,6 +88,16 @@
     enum_constant public static final com.android.internal.vibrator.persistence.WaveformAmplitudeDefault _default;
   }
 
+  public class WaveformControlPoint {
+    ctor public WaveformControlPoint();
+    method public float getAmplitude();
+    method public long getDurationMs();
+    method public float getFrequencyHz();
+    method public void setAmplitude(float);
+    method public void setDurationMs(long);
+    method public void setFrequencyHz(float);
+  }
+
   public class WaveformEffect {
     ctor public WaveformEffect();
     method public com.android.internal.vibrator.persistence.WaveformEffect.Repeating getRepeating();
@@ -87,6 +118,13 @@
     method public void setDurationMs(java.math.BigInteger);
   }
 
+  public class WaveformEnvelopeEffect {
+    ctor public WaveformEnvelopeEffect();
+    method public java.util.List<com.android.internal.vibrator.persistence.WaveformControlPoint> getControlPoint();
+    method public float getInitialFrequencyHz();
+    method public void setInitialFrequencyHz(float);
+  }
+
   public class XmlParser {
     ctor public XmlParser();
     method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
index 910a9b7..b4df2d1 100644
--- a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
+++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
@@ -54,6 +54,12 @@
                 <xs:element name="primitive-effect" type="PrimitiveEffect"/>
             </xs:sequence>
 
+            <!-- Waveform envelope effect -->
+            <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect"/>
+
+            <!-- Basic envelope effect -->
+            <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
+
         </xs:choice>
     </xs:complexType>
 
@@ -180,4 +186,54 @@
         </xs:restriction>
     </xs:simpleType>
 
+    <!-- Definition of a waveform envelope effect -->
+    <xs:complexType name="WaveformEnvelopeEffect">
+        <xs:sequence>
+            <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+                type="WaveformControlPoint" />
+        </xs:sequence>
+        <xs:attribute name="initialFrequencyHz" type="ControlPointFrequency" />
+    </xs:complexType>
+
+    <!-- Definition of a basic envelope effect -->
+    <xs:complexType name="BasicEnvelopeEffect">
+        <xs:sequence>
+            <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+                type="BasicControlPoint" />
+        </xs:sequence>
+        <xs:attribute name="initialSharpness" type="NormalizedControlPointUnit" />
+    </xs:complexType>
+
+    <xs:complexType name="WaveformControlPoint">
+        <xs:attribute name="amplitude" type="NormalizedControlPointUnit" use="required"/>
+        <xs:attribute name="frequencyHz" type="ControlPointFrequency" use="required"/>
+        <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+    </xs:complexType>
+
+    <xs:complexType name="BasicControlPoint">
+        <xs:attribute name="intensity" type="NormalizedControlPointUnit" use="required"/>
+        <xs:attribute name="sharpness" type="NormalizedControlPointUnit" use="required"/>
+        <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+    </xs:complexType>
+
+    <xs:simpleType name="ControlPointFrequency">
+        <xs:restriction base="xs:float">
+            <xs:minExclusive value="0"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <xs:simpleType name="PositiveLong">
+        <xs:restriction base="xs:long">
+            <xs:minExclusive value="0"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <!-- Normalized control point unit float in [0,1] -->
+    <xs:simpleType name="NormalizedControlPointUnit">
+        <xs:restriction base="xs:float">
+            <xs:minInclusive value="0"/>
+            <xs:maxInclusive value="1"/>
+        </xs:restriction>
+    </xs:simpleType>
+
 </xs:schema>
diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd
index 3c8e016..fba966f 100644
--- a/core/xsd/vibrator/vibration/vibration.xsd
+++ b/core/xsd/vibrator/vibration/vibration.xsd
@@ -52,6 +52,12 @@
                 <xs:element name="primitive-effect" type="PrimitiveEffect"/>
             </xs:sequence>
 
+            <!-- Waveform envelope effect -->
+            <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect"/>
+
+            <!-- Basic envelope effect -->
+            <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
+
         </xs:choice>
     </xs:complexType>
 
@@ -157,4 +163,54 @@
         </xs:restriction>
     </xs:simpleType>
 
+    <!-- Definition of a waveform envelope effect -->
+    <xs:complexType name="WaveformEnvelopeEffect">
+        <xs:sequence>
+            <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+                type="WaveformControlPoint" />
+        </xs:sequence>
+        <xs:attribute name="initialFrequencyHz" type="ControlPointFrequency" />
+    </xs:complexType>
+
+    <!-- Definition of a basic envelope effect -->
+    <xs:complexType name="BasicEnvelopeEffect">
+        <xs:sequence>
+            <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+                type="BasicControlPoint" />
+        </xs:sequence>
+        <xs:attribute name="initialSharpness" type="NormalizedControlPointUnit" />
+    </xs:complexType>
+
+    <xs:complexType name="WaveformControlPoint">
+        <xs:attribute name="amplitude" type="NormalizedControlPointUnit" use="required"/>
+        <xs:attribute name="frequencyHz" type="ControlPointFrequency" use="required"/>
+        <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+    </xs:complexType>
+
+    <xs:complexType name="BasicControlPoint">
+        <xs:attribute name="intensity" type="NormalizedControlPointUnit" use="required"/>
+        <xs:attribute name="sharpness" type="NormalizedControlPointUnit" use="required"/>
+        <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+    </xs:complexType>
+
+    <xs:simpleType name="ControlPointFrequency">
+        <xs:restriction base="xs:float">
+            <xs:minExclusive value="0"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <xs:simpleType name="PositiveLong">
+        <xs:restriction base="xs:long">
+            <xs:minExclusive value="0"/>
+        </xs:restriction>
+    </xs:simpleType>
+
+    <!-- Normalized control point unit float in [0,1] -->
+    <xs:simpleType name="NormalizedControlPointUnit">
+        <xs:restriction base="xs:float">
+            <xs:minInclusive value="0"/>
+            <xs:maxInclusive value="1"/>
+        </xs:restriction>
+    </xs:simpleType>
+
 </xs:schema>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerStrategy.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerStrategy.kt
new file mode 100644
index 0000000..9e3edf6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerStrategy.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 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.wm.shell.compatui.letterbox
+
+import com.android.wm.shell.dagger.WMSingleton
+import javax.inject.Inject
+
+/**
+ * Encapsulate the logic related to the use of a single or multiple surfaces when
+ * implementing letterbox in shell.
+ */
+@WMSingleton
+class LetterboxControllerStrategy @Inject constructor() {
+
+    // Different letterbox implementation modes.
+    enum class LetterboxMode { SINGLE_SURFACE, MULTIPLE_SURFACES }
+
+    @Volatile
+    private var currentMode: LetterboxMode = LetterboxMode.SINGLE_SURFACE
+
+    fun configureLetterboxMode() {
+        // TODO(b/377875146): Define criteria for switching between [LetterboxMode]s.
+        currentMode = if (android.os.SystemProperties.getInt(
+                "multi_interface",
+                0
+            ) == 0
+        ) {
+            LetterboxMode.SINGLE_SURFACE
+        } else {
+            LetterboxMode.MULTIPLE_SURFACES
+        }
+    }
+
+    /**
+     * @return The specific mode to use for implementing letterboxing for the given [request].
+     */
+    fun getLetterboxImplementationMode(): LetterboxMode = currentMode
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt
index a48dc8b..4718071 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt
@@ -35,7 +35,8 @@
     shellInit: ShellInit,
     private val transitions: Transitions,
     private val letterboxController: LetterboxController,
-    private val transitionStateHolder: TransitionStateHolder
+    private val transitionStateHolder: TransitionStateHolder,
+    private val letterboxModeStrategy: LetterboxControllerStrategy
 ) : Transitions.TransitionObserver {
 
     companion object {
@@ -63,7 +64,6 @@
         // We recognise the operation to execute and delegate to the LetterboxController
         // the related operation.
         // TODO(b/377875151): Identify Desktop Windowing Transactions.
-        // TODO(b/377857898): Handling multiple surfaces
         // TODO(b/371500295): Handle input events detection.
         for (change in info.changes) {
             change.taskInfo?.let { ti ->
@@ -83,6 +83,7 @@
                     } else {
                         val isTopActivityLetterboxed = ti.appCompatTaskInfo.isTopActivityLetterboxed
                         if (isTopActivityLetterboxed) {
+                            letterboxModeStrategy.configureLetterboxMode()
                             createLetterboxSurface(
                                 key,
                                 startTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxUtils.kt
new file mode 100644
index 0000000..ef964f4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxUtils.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 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.wm.shell.compatui.letterbox
+
+import android.graphics.Rect
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+
+/**
+ * Creates a [LetterboxController] which is the composition of other two [LetterboxController].
+ * It basically invokes the method on both of them.
+ */
+infix fun LetterboxController.append(other: LetterboxController) = object : LetterboxController {
+    override fun createLetterboxSurface(
+        key: LetterboxKey,
+        transaction: Transaction,
+        parentLeash: SurfaceControl
+    ) {
+        this@append.createLetterboxSurface(key, transaction, parentLeash)
+        other.createLetterboxSurface(key, transaction, parentLeash)
+    }
+
+    override fun destroyLetterboxSurface(
+        key: LetterboxKey,
+        transaction: Transaction
+    ) {
+        this@append.destroyLetterboxSurface(key, transaction)
+        other.destroyLetterboxSurface(key, transaction)
+    }
+
+    override fun updateLetterboxSurfaceVisibility(
+        key: LetterboxKey,
+        transaction: Transaction,
+        visible: Boolean
+    ) {
+        this@append.updateLetterboxSurfaceVisibility(key, transaction, visible)
+        other.updateLetterboxSurfaceVisibility(key, transaction, visible)
+    }
+
+    override fun updateLetterboxSurfaceBounds(
+        key: LetterboxKey,
+        transaction: Transaction,
+        taskBounds: Rect,
+        activityBounds: Rect
+    ) {
+        this@append.updateLetterboxSurfaceBounds(key, transaction, taskBounds, activityBounds)
+        other.updateLetterboxSurfaceBounds(key, transaction, taskBounds, activityBounds)
+    }
+
+    override fun dump() {
+        this@append.dump()
+        other.dump()
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxController.kt
new file mode 100644
index 0000000..8d06570
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxController.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 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.wm.shell.compatui.letterbox
+
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy.LetterboxMode.MULTIPLE_SURFACES
+import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy.LetterboxMode.SINGLE_SURFACE
+import com.android.wm.shell.dagger.WMSingleton
+import javax.inject.Inject
+
+/**
+ * [LetterboxController] implementation working as coordinator of other [LetterboxController]
+ * implementations.
+ */
+@WMSingleton
+class MixedLetterboxController @Inject constructor(
+    private val singleSurfaceController: SingleSurfaceLetterboxController,
+    private val multipleSurfaceController: MultiSurfaceLetterboxController,
+    private val controllerStrategy: LetterboxControllerStrategy
+) : LetterboxController by singleSurfaceController append multipleSurfaceController {
+
+    override fun createLetterboxSurface(
+        key: LetterboxKey,
+        transaction: Transaction,
+        parentLeash: SurfaceControl
+    ) {
+        when (controllerStrategy.getLetterboxImplementationMode()) {
+            SINGLE_SURFACE -> {
+                multipleSurfaceController.destroyLetterboxSurface(key, transaction)
+                singleSurfaceController.createLetterboxSurface(key, transaction, parentLeash)
+            }
+
+            MULTIPLE_SURFACES -> {
+                singleSurfaceController.destroyLetterboxSurface(key, transaction)
+                multipleSurfaceController.createLetterboxSurface(key, transaction, parentLeash)
+            }
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index df2b849..806be8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -74,8 +74,9 @@
 import com.android.wm.shell.common.transition.TransitionStateHolder;
 import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler;
 import com.android.wm.shell.compatui.letterbox.LetterboxController;
+import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy;
 import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver;
-import com.android.wm.shell.compatui.letterbox.SingleSurfaceLetterboxController;
+import com.android.wm.shell.compatui.letterbox.MixedLetterboxController;
 import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
 import com.android.wm.shell.dagger.pip.PipModule;
 import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
@@ -1338,14 +1339,15 @@
             @NonNull ShellInit shellInit,
             @NonNull Transitions transitions,
             @NonNull LetterboxController letterboxController,
-            @NonNull TransitionStateHolder transitionStateHolder
+            @NonNull TransitionStateHolder transitionStateHolder,
+            @NonNull LetterboxControllerStrategy letterboxControllerStrategy
     ) {
         return new LetterboxTransitionObserver(shellInit, transitions, letterboxController,
-                transitionStateHolder);
+                transitionStateHolder, letterboxControllerStrategy);
     }
 
     @WMSingleton
     @Binds
     abstract LetterboxController bindsLetterboxController(
-            SingleSurfaceLetterboxController letterboxController);
+            MixedLetterboxController letterboxController);
 }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt
index f9f01bc..95a0c82 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt
@@ -98,6 +98,10 @@
         activityBounds = activityBounds
     )
 
+    fun invokeDump() {
+        letterboxController.dump()
+    }
+
     fun checkSurfaceBuilderInvoked(times: Int = 1, name: String = "", callSite: String = "") {
         verify(surfaceBuilder, times(times)).createSurface(
             eq(transaction),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt
index 8825bb4..78bb721 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt
@@ -96,6 +96,7 @@
 
                 validateOutput {
                     r.creationEventDetected(expected = false)
+                    r.configureStrategyInvoked(expected = false)
                     r.visibilityEventDetected(expected = false)
                     r.destroyEventDetected(expected = false)
                     r.updateSurfaceBoundsEventDetected(expected = false)
@@ -123,6 +124,7 @@
 
                 validateOutput {
                     r.creationEventDetected(expected = true)
+                    r.configureStrategyInvoked(expected = true)
                     r.visibilityEventDetected(expected = true, visible = true)
                     r.destroyEventDetected(expected = false)
                     r.updateSurfaceBoundsEventDetected(
@@ -217,6 +219,7 @@
         private val letterboxController: LetterboxController
         private val letterboxObserver: LetterboxTransitionObserver
         private val transitionStateHolder: TransitionStateHolder
+        private val letterboxStrategy: LetterboxControllerStrategy
 
         val observerFactory: () -> LetterboxTransitionObserver
 
@@ -225,6 +228,7 @@
             shellInit = ShellInit(executor)
             transitions = mock<Transitions>()
             letterboxController = mock<LetterboxController>()
+            letterboxStrategy = mock<LetterboxControllerStrategy>()
             transitionStateHolder =
                 TransitionStateHolder(shellInit, mock<RecentsTransitionHandler>())
             spyOn(transitionStateHolder)
@@ -233,7 +237,8 @@
                     shellInit,
                     transitions,
                     letterboxController,
-                    transitionStateHolder
+                    transitionStateHolder,
+                    letterboxStrategy
                 )
             observerFactory = { letterboxObserver }
         }
@@ -302,6 +307,9 @@
             eq(activityBounds)
         )
 
+        fun configureStrategyInvoked(expected: Boolean) =
+            verify(letterboxStrategy, expected.asMode()).configureLetterboxMode()
+
         fun createTopActivityChange(
             inputBuilder: TransitionObserverInputBuilder,
             isLetterboxed: Boolean = true,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxUtilsTest.kt
new file mode 100644
index 0000000..06b8052
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxUtilsTest.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 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.wm.shell.compatui.letterbox
+
+import android.content.Context
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import java.util.function.Consumer
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [LetterboxUtils].
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:LetterboxUtilsTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LetterboxUtilsTest : ShellTestCase() {
+
+    val firstLetterboxController = mock<LetterboxController>()
+    val secondLetterboxController = mock<LetterboxController>()
+    val thirdLetterboxController = mock<LetterboxController>()
+
+    private val letterboxControllerBuilder: (LetterboxSurfaceBuilder) -> LetterboxController =
+        { _ ->
+            firstLetterboxController.append(secondLetterboxController)
+                .append(thirdLetterboxController)
+        }
+
+    @Test
+    fun `Appended LetterboxController invoked creation on all the controllers`() {
+        runTestScenario { r ->
+            r.sendCreateSurfaceRequest()
+
+            r.verifyCreateSurfaceInvokedWithRequest(target = firstLetterboxController)
+            r.verifyCreateSurfaceInvokedWithRequest(target = secondLetterboxController)
+            r.verifyCreateSurfaceInvokedWithRequest(target = thirdLetterboxController)
+        }
+    }
+
+    @Test
+    fun `Appended LetterboxController invoked destroy on all the controllers`() {
+        runTestScenario { r ->
+            r.sendDestroySurfaceRequest()
+            r.verifyDestroySurfaceInvokedWithRequest(target = firstLetterboxController)
+            r.verifyDestroySurfaceInvokedWithRequest(target = secondLetterboxController)
+            r.verifyDestroySurfaceInvokedWithRequest(target = thirdLetterboxController)
+        }
+    }
+
+    @Test
+    fun `Appended LetterboxController invoked update visibility on all the controllers`() {
+        runTestScenario { r ->
+            r.sendUpdateSurfaceVisibilityRequest(visible = true)
+            r.verifyUpdateVisibilitySurfaceInvokedWithRequest(target = firstLetterboxController)
+            r.verifyUpdateVisibilitySurfaceInvokedWithRequest(target = secondLetterboxController)
+            r.verifyUpdateVisibilitySurfaceInvokedWithRequest(target = thirdLetterboxController)
+        }
+    }
+
+    @Test
+    fun `Appended LetterboxController invoked update bounds on all the controllers`() {
+        runTestScenario { r ->
+            r.sendUpdateSurfaceBoundsRequest(taskBounds = Rect(), activityBounds = Rect())
+            r.verifyUpdateSurfaceBoundsInvokedWithRequest(target = firstLetterboxController)
+            r.verifyUpdateSurfaceBoundsInvokedWithRequest(target = secondLetterboxController)
+            r.verifyUpdateSurfaceBoundsInvokedWithRequest(target = thirdLetterboxController)
+        }
+    }
+
+    @Test
+    fun `Appended LetterboxController invoked update dump on all the controllers`() {
+        runTestScenario { r ->
+            r.invokeDump()
+            r.verifyDumpInvoked(target = firstLetterboxController)
+            r.verifyDumpInvoked(target = secondLetterboxController)
+            r.verifyDumpInvoked(target = thirdLetterboxController)
+        }
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    fun runTestScenario(consumer: Consumer<AppendLetterboxControllerRobotTest>) {
+        val robot = AppendLetterboxControllerRobotTest(mContext, letterboxControllerBuilder)
+        consumer.accept(robot)
+    }
+
+    class AppendLetterboxControllerRobotTest(
+        ctx: Context,
+        builder: (LetterboxSurfaceBuilder) -> LetterboxController
+    ) : LetterboxControllerRobotTest(ctx, builder) {
+
+        fun verifyCreateSurfaceInvokedWithRequest(
+            target: LetterboxController,
+            times: Int = 1
+        ) {
+            verify(target, times(times)).createLetterboxSurface(any(), any(), any())
+        }
+
+        fun verifyDestroySurfaceInvokedWithRequest(
+            target: LetterboxController,
+            times: Int = 1
+        ) {
+            verify(target, times(times)).destroyLetterboxSurface(any(), any())
+        }
+
+        fun verifyUpdateVisibilitySurfaceInvokedWithRequest(
+            target: LetterboxController,
+            times: Int = 1
+        ) {
+            verify(target, times(times)).updateLetterboxSurfaceVisibility(any(), any(), any())
+        }
+
+        fun verifyUpdateSurfaceBoundsInvokedWithRequest(
+            target: LetterboxController,
+            times: Int = 1
+        ) {
+            verify(target, times(times)).updateLetterboxSurfaceBounds(any(), any(), any(), any())
+        }
+
+        fun verifyDumpInvoked(
+            target: LetterboxController,
+            times: Int = 1
+        ) {
+            verify(target, times(times)).dump()
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxControllerTest.kt
new file mode 100644
index 0000000..e6bff4c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxControllerTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 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.wm.shell.compatui.letterbox
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy.LetterboxMode
+import java.util.function.Consumer
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [MixedLetterboxController].
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:MixedLetterboxControllerTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class MixedLetterboxControllerTest : ShellTestCase() {
+
+    @Test
+    fun `When strategy is SINGLE_SURFACE and a create request is sent multi are destroyed`() {
+        runTestScenario { r ->
+            r.configureStrategyFor(LetterboxMode.SINGLE_SURFACE)
+            r.sendCreateSurfaceRequest()
+            r.checkCreateInvokedOnSingleController()
+            r.checkDestroyInvokedOnMultiController()
+        }
+    }
+
+    @Test
+    fun `When strategy is MULTIPLE_SURFACES and a create request is sent single is destroyed`() {
+        runTestScenario { r ->
+            r.configureStrategyFor(LetterboxMode.MULTIPLE_SURFACES)
+            r.sendCreateSurfaceRequest()
+            r.checkDestroyInvokedOnSingleController()
+            r.checkCreateInvokedOnMultiController()
+        }
+    }
+
+    /**
+     * Runs a test scenario providing a Robot.
+     */
+    fun runTestScenario(consumer: Consumer<MixedLetterboxControllerRobotTest>) {
+        val robot = MixedLetterboxControllerRobotTest(mContext, ObjectToTestHolder())
+        consumer.accept(robot)
+    }
+
+    class MixedLetterboxControllerRobotTest(
+        ctx: Context,
+        private val objectToTestHolder: ObjectToTestHolder
+    ) : LetterboxControllerRobotTest(ctx, objectToTestHolder.controllerBuilder) {
+
+        fun configureStrategyFor(letterboxMode: LetterboxMode) {
+            doReturn(letterboxMode).`when`(objectToTestHolder.controllerStrategy)
+                .getLetterboxImplementationMode()
+        }
+
+        fun checkCreateInvokedOnSingleController(times: Int = 1) {
+            verify(
+                objectToTestHolder.singleLetterboxController,
+                times(times)
+            ).createLetterboxSurface(any(), any(), any())
+        }
+
+        fun checkCreateInvokedOnMultiController(times: Int = 1) {
+            verify(
+                objectToTestHolder.multipleLetterboxController,
+                times(times)
+            ).createLetterboxSurface(any(), any(), any())
+        }
+
+        fun checkDestroyInvokedOnSingleController(times: Int = 1) {
+            verify(
+                objectToTestHolder.singleLetterboxController,
+                times(times)
+            ).destroyLetterboxSurface(any(), any())
+        }
+
+        fun checkDestroyInvokedOnMultiController(times: Int = 1) {
+            verify(
+                objectToTestHolder.multipleLetterboxController,
+                times(times)
+            ).destroyLetterboxSurface(any(), any())
+        }
+    }
+
+    data class ObjectToTestHolder(
+        val singleLetterboxController: SingleSurfaceLetterboxController =
+        mock<SingleSurfaceLetterboxController>(),
+        val multipleLetterboxController: MultiSurfaceLetterboxController =
+        mock<MultiSurfaceLetterboxController>(),
+        val controllerStrategy: LetterboxControllerStrategy = mock<LetterboxControllerStrategy>()
+    ) {
+
+        private val mixedController =
+            MixedLetterboxController(
+                singleLetterboxController,
+                multipleLetterboxController,
+                controllerStrategy
+            )
+
+        val controllerBuilder: (LetterboxSurfaceBuilder) -> LetterboxController =
+            { _ -> mixedController }
+    }
+}
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index d993b87..28d96e3 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -28,12 +28,14 @@
 #define INDENT "  "
 #define INDENT2 "    "
 
+namespace android {
+
 namespace {
+
 // Time to spend fading out the pointer completely.
 const nsecs_t POINTER_FADE_DURATION = 500 * 1000000LL; // 500 ms
-} // namespace
 
-namespace android {
+} // namespace
 
 // --- MouseCursorController ---
 
@@ -64,17 +66,23 @@
     mLocked.pointerSprite.clear();
 }
 
-void MouseCursorController::move(float deltaX, float deltaY) {
+FloatPoint MouseCursorController::move(float deltaX, float deltaY) {
 #if DEBUG_MOUSE_CURSOR_UPDATES
     ALOGD("Move pointer by deltaX=%0.3f, deltaY=%0.3f", deltaX, deltaY);
 #endif
     if (deltaX == 0.0f && deltaY == 0.0f) {
-        return;
+        return {0, 0};
     }
 
+    // When transition occurs, the MouseCursorController object may or may not be deleted, depending
+    // if there's another display on the other side of the transition. At this point we still need
+    // to move the cursor to the boundary.
     std::scoped_lock lock(mLock);
-
-    setPositionLocked(mLocked.pointerX + deltaX, mLocked.pointerY + deltaY);
+    const FloatPoint position{mLocked.pointerX + deltaX, mLocked.pointerY + deltaY};
+    setPositionLocked(position.x, position.y);
+    // The amount of the delta that was not consumed as a result of the cursor
+    // hitting the edge of the display.
+    return {position.x - mLocked.pointerX, position.y - mLocked.pointerY};
 }
 
 void MouseCursorController::setPosition(float x, float y) {
@@ -85,19 +93,23 @@
     setPositionLocked(x, y);
 }
 
-void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) {
-    const auto& v = mLocked.viewport;
-    if (!v.isValid()) return;
-
+FloatRect MouseCursorController::getBoundsLocked() REQUIRES(mLock) {
     // The valid bounds for a mouse cursor. Since the right and bottom edges are considered outside
     // the display, clip the bounds by one pixel instead of letting the cursor get arbitrarily
     // close to the outside edge.
-    const FloatRect bounds{
+    return FloatRect{
             static_cast<float>(mLocked.viewport.logicalLeft),
             static_cast<float>(mLocked.viewport.logicalTop),
             static_cast<float>(mLocked.viewport.logicalRight - 1),
             static_cast<float>(mLocked.viewport.logicalBottom - 1),
     };
+}
+
+void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) {
+    const auto& v = mLocked.viewport;
+    if (!v.isValid()) return;
+
+    const FloatRect bounds = getBoundsLocked();
     mLocked.pointerX = std::max(bounds.left, std::min(bounds.right, x));
     mLocked.pointerY = std::max(bounds.top, std::min(bounds.bottom, y));
 
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index 12b31a8..e14a55d 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -20,9 +20,6 @@
 #include <gui/DisplayEventReceiver.h>
 #include <input/DisplayViewport.h>
 #include <input/Input.h>
-#include <utils/BitSet.h>
-#include <utils/Looper.h>
-#include <utils/RefBase.h>
 
 #include <functional>
 #include <map>
@@ -43,7 +40,8 @@
     MouseCursorController(PointerControllerContext& context);
     ~MouseCursorController();
 
-    void move(float deltaX, float deltaY);
+    // Move the pointer and return unconsumed delta
+    FloatPoint move(float deltaX, float deltaY);
     void setPosition(float x, float y);
     FloatPoint getPosition() const;
     ui::LogicalDisplayId getDisplayId() const;
@@ -113,6 +111,7 @@
     bool doFadingAnimationLocked(nsecs_t timestamp);
 
     void startAnimationLocked();
+    FloatRect getBoundsLocked();
 };
 
 } // namespace android
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 78d7d3a..a713f1d 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -138,15 +138,19 @@
     return mDisplayInfoListener->mLock;
 }
 
-void PointerController::move(float deltaX, float deltaY) {
+FloatPoint PointerController::move(float deltaX, float deltaY) {
     const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
-    vec2 transformed;
+    ui::Transform transform;
     {
         std::scoped_lock lock(getLock());
-        const auto& transform = getTransformForDisplayLocked(displayId);
-        transformed = transformWithoutTranslation(transform, {deltaX, deltaY});
+        transform = getTransformForDisplayLocked(displayId);
     }
-    mCursorController.move(transformed.x, transformed.y);
+
+    const vec2 transformed = transformWithoutTranslation(transform, {deltaX, deltaY});
+
+    const FloatPoint unconsumedDelta = mCursorController.move(transformed.x, transformed.y);
+    return FloatPoint(transformWithoutTranslation(transform.inverse(),
+                                                  {unconsumedDelta.x, unconsumedDelta.y}));
 }
 
 void PointerController::setPosition(float x, float y) {
@@ -295,6 +299,11 @@
     mCursorController.setSkipScreenshot(false);
 }
 
+ui::Transform PointerController::getDisplayTransform() const {
+    std::scoped_lock lock(getLock());
+    return getTransformForDisplayLocked(mLocked.pointerDisplayId);
+}
+
 void PointerController::doInactivityTimeout() {
     fade(Transition::GRADUAL);
 }
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index ee8d121..8b33190 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -51,7 +51,7 @@
 
     ~PointerController() override;
 
-    void move(float deltaX, float deltaY) override;
+    FloatPoint move(float deltaX, float deltaY) override;
     void setPosition(float x, float y) override;
     FloatPoint getPosition() const override;
     ui::LogicalDisplayId getDisplayId() const override;
@@ -67,6 +67,7 @@
     void setCustomPointerIcon(const SpriteIcon& icon) override;
     void setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) override;
     void clearSkipScreenshotFlags() override;
+    ui::Transform getDisplayTransform() const override;
 
     virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout);
     void doInactivityTimeout();
@@ -165,7 +166,7 @@
 
     ~TouchPointerController() override;
 
-    void move(float, float) override {
+    FloatPoint move(float, float) override {
         LOG_ALWAYS_FATAL("Should not be called");
     }
     void setPosition(float, float) override {
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index 5b00fca..80c934a 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -40,6 +40,8 @@
     CURSOR_TYPE_CUSTOM = -1,
 };
 
+static constexpr float EPSILON = MotionEvent::ROUNDING_PRECISION;
+
 using ::testing::AllOf;
 using ::testing::Field;
 using ::testing::NiceMock;
@@ -399,6 +401,135 @@
                          testing::Values(PointerControllerInterface::ControllerType::MOUSE,
                                          PointerControllerInterface::ControllerType::STYLUS));
 
+class MousePointerControllerTest : public PointerControllerTest {
+protected:
+    MousePointerControllerTest() {
+        sp<MockSprite> testPointerSprite(new NiceMock<MockSprite>);
+        EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testPointerSprite));
+
+        // create a mouse pointer controller
+        mPointerController =
+                PointerController::create(mPolicy, mLooper, *mSpriteController,
+                                          PointerControllerInterface::ControllerType::MOUSE);
+
+        // set display viewport
+        DisplayViewport viewport;
+        viewport.displayId = ui::LogicalDisplayId::DEFAULT;
+        viewport.logicalRight = 5;
+        viewport.logicalBottom = 5;
+        viewport.physicalRight = 5;
+        viewport.physicalBottom = 5;
+        viewport.deviceWidth = 5;
+        viewport.deviceHeight = 5;
+        mPointerController->setDisplayViewport(viewport);
+    }
+};
+
+struct MousePointerControllerTestParam {
+    vec2 startPosition;
+    vec2 delta;
+    vec2 endPosition;
+    vec2 unconsumedDelta;
+};
+
+class PointerControllerViewportTransitionTest
+      : public MousePointerControllerTest,
+        public testing::WithParamInterface<MousePointerControllerTestParam> {};
+
+TEST_P(PointerControllerViewportTransitionTest, testPointerViewportTransition) {
+    const auto& params = GetParam();
+
+    mPointerController->setPosition(params.startPosition.x, params.startPosition.y);
+    auto unconsumedDelta = mPointerController->move(params.delta.x, params.delta.y);
+
+    auto position = mPointerController->getPosition();
+    EXPECT_NEAR(position.x, params.endPosition.x, EPSILON);
+    EXPECT_NEAR(position.y, params.endPosition.y, EPSILON);
+    EXPECT_NEAR(unconsumedDelta.x, params.unconsumedDelta.x, EPSILON);
+    EXPECT_NEAR(unconsumedDelta.y, params.unconsumedDelta.y, EPSILON);
+}
+
+INSTANTIATE_TEST_SUITE_P(PointerControllerViewportTransitionTest,
+                         PointerControllerViewportTransitionTest,
+                         testing::Values(
+                                 // no transition
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {2.0f, 2.0f},
+                                                                 {4.0f, 4.0f},
+                                                                 {0.0f, 0.0f}},
+                                 // right boundary
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {3.0f, 0.0f},
+                                                                 {4.0f, 2.0f},
+                                                                 {1.0f, 0.0f}},
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {3.0f, -1.0f},
+                                                                 {4.0f, 1.0f},
+                                                                 {1.0f, 0.0f}},
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {3.0f, 1.0f},
+                                                                 {4.0f, 3.0f},
+                                                                 {1.0f, 0.0f}},
+                                 // left boundary
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {-3.0f, 0.0f},
+                                                                 {0.0f, 2.0f},
+                                                                 {-1.0f, 0.0f}},
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {-3.0f, -1.0f},
+                                                                 {0.0f, 1.0f},
+                                                                 {-1.0f, 0.0f}},
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {-3.0f, 1.0f},
+                                                                 {0.0f, 3.0f},
+                                                                 {-1.0f, 0.0f}},
+                                 // bottom boundary
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {0.0f, 3.0f},
+                                                                 {2.0f, 4.0f},
+                                                                 {0.0f, 1.0f}},
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {-1.0f, 3.0f},
+                                                                 {1.0f, 4.0f},
+                                                                 {0.0f, 1.0f}},
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {1.0f, 3.0f},
+                                                                 {3.0f, 4.0f},
+                                                                 {0.0f, 1.0f}},
+                                 // top boundary
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {0.0f, -3.0f},
+                                                                 {2.0f, 0.0f},
+                                                                 {0.0f, -1.0f}},
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {-1.0f, -3.0f},
+                                                                 {1.0f, 0.0f},
+                                                                 {0.0f, -1.0f}},
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {1.0f, -3.0f},
+                                                                 {3.0f, 0.0f},
+                                                                 {0.0f, -1.0f}},
+                                 // top-left corner
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {-3.0f, -3.0f},
+                                                                 {0.0f, 0.0f},
+                                                                 {-1.0f, -1.0f}},
+                                 // top-right corner
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {3.0f, -3.0f},
+                                                                 {4.0f, 0.0f},
+                                                                 {1.0f, -1.0f}},
+                                 // bottom-right corner
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {3.0f, 3.0f},
+                                                                 {4.0f, 4.0f},
+                                                                 {1.0f, 1.0f}},
+                                 // bottom-left corner
+                                 MousePointerControllerTestParam{{2.0f, 2.0f},
+                                                                 {-3.0f, 3.0f},
+                                                                 {0.0f, 4.0f},
+                                                                 {-1.0f, 1.0f}}));
+
 class PointerControllerWindowInfoListenerTest : public Test {};
 
 TEST_F(PointerControllerWindowInfoListenerTest,
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index f7f10df..4f7132a 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -324,6 +324,19 @@
     }
 
     /**
+     * Stops projection.
+     * @hide
+     */
+    public void stop(@StopReason int stopReason) {
+        try {
+            Log.d(TAG, "Content Recording: stopping projection");
+            mImpl.stop(stopReason);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to stop projection", e);
+        }
+    }
+
+    /**
      * Get the underlying IMediaProjection.
      * @hide
      */
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
index 77be8c7..6ec38ba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
@@ -16,8 +16,9 @@
 
 package com.android.systemui.mediarouter.data.repository
 
-import androidx.test.filters.SmallTest
+import android.media.projection.StopReason
 import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.kosmos.Kosmos
@@ -101,7 +102,7 @@
                 origin = CastDevice.CastOrigin.MediaRouter,
             )
 
-        underTest.stopCasting(device)
+        underTest.stopCasting(device, StopReason.STOP_UNKNOWN)
 
         assertThat(castController.lastStoppedDevice).isEqualTo(device)
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
index 9f12b18..31a627f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
@@ -20,6 +20,7 @@
 import static junit.framework.TestCase.assertEquals;
 
 import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.mock;
@@ -30,6 +31,7 @@
 import android.media.MediaRouter;
 import android.media.MediaRouter.RouteInfo;
 import android.media.projection.MediaProjectionInfo;
+import android.media.projection.StopReason;
 import android.os.Handler;
 import android.service.quicksettings.Tile;
 import android.testing.TestableLooper;
@@ -336,7 +338,8 @@
         mCastTile.handleClick(null /* view */);
         mTestableLooper.processAllMessages();
 
-        verify(mController, times(1)).stopCasting(same(device));
+        verify(mController, times(1))
+                .stopCasting(same(device), eq(StopReason.STOP_QS_TILE));
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
index 53708fd..6ebe830 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
@@ -23,11 +23,13 @@
 import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.Dialog;
+import android.media.projection.StopReason;
 import android.os.Handler;
 import android.service.quicksettings.Tile;
 import android.testing.TestableLooper;
@@ -214,7 +216,7 @@
 
         mTile.handleClick(null /* view */);
 
-        verify(mController, times(1)).stopRecording();
+        verify(mController, times(1)).stopRecording(eq(StopReason.STOP_QS_TILE));
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
index 0b56d7b..778c73f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs.tiles.impl.screenrecord.domain.interactor
 
 import android.app.Dialog
+import android.media.projection.StopReason
 import android.os.UserHandle
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
@@ -92,7 +93,7 @@
 
         underTest.handleInput(QSTileInputTestKtx.click(recordingModel))
 
-        verify(recordingController).stopRecording()
+        verify(recordingController).stopRecording(eq(StopReason.STOP_QS_TILE))
     }
 
     @Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index a6a1d4a..50fa9d2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -41,6 +41,7 @@
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.content.Intent;
+import android.media.projection.StopReason;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -199,16 +200,16 @@
     public void testOnSystemRequestedStop_recordingInProgress_endsRecording() throws IOException {
         doReturn(true).when(mController).isRecording();
 
-        mRecordingService.onStopped();
+        mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
 
-        verify(mScreenMediaRecorder).end();
+        verify(mScreenMediaRecorder).end(eq(StopReason.STOP_UNKNOWN));
     }
 
     @Test
     public void testOnSystemRequestedStop_recordingInProgress_updatesState() {
         doReturn(true).when(mController).isRecording();
 
-        mRecordingService.onStopped();
+        mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
 
         assertUpdateState(false);
     }
@@ -218,18 +219,18 @@
             throws IOException {
         doReturn(false).when(mController).isRecording();
 
-        mRecordingService.onStopped();
+        mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
 
-        verify(mScreenMediaRecorder, never()).end();
+        verify(mScreenMediaRecorder, never()).end(StopReason.STOP_UNKNOWN);
     }
 
     @Test
     public void testOnSystemRequestedStop_recorderEndThrowsRuntimeException_releasesRecording()
             throws IOException {
         doReturn(true).when(mController).isRecording();
-        doThrow(new RuntimeException()).when(mScreenMediaRecorder).end();
+        doThrow(new RuntimeException()).when(mScreenMediaRecorder).end(StopReason.STOP_UNKNOWN);
 
-        mRecordingService.onStopped();
+        mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
 
         verify(mScreenMediaRecorder).release();
     }
@@ -238,7 +239,7 @@
     public void testOnSystemRequestedStop_whenRecordingInProgress_showsNotifications() {
         doReturn(true).when(mController).isRecording();
 
-        mRecordingService.onStopped();
+        mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
 
         // Processing notification
         ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class);
@@ -271,9 +272,9 @@
     public void testOnSystemRequestedStop_recorderEndThrowsRuntimeException_showsErrorNotification()
             throws IOException {
         doReturn(true).when(mController).isRecording();
-        doThrow(new RuntimeException()).when(mScreenMediaRecorder).end();
+        doThrow(new RuntimeException()).when(mScreenMediaRecorder).end(anyInt());
 
-        mRecordingService.onStopped();
+        mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
 
         verify(mRecordingService).createErrorSavingNotification(any());
         ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class);
@@ -289,9 +290,9 @@
     public void testOnSystemRequestedStop_recorderEndThrowsOOMError_releasesRecording()
             throws IOException {
         doReturn(true).when(mController).isRecording();
-        doThrow(new OutOfMemoryError()).when(mScreenMediaRecorder).end();
+        doThrow(new OutOfMemoryError()).when(mScreenMediaRecorder).end(anyInt());
 
-        assertThrows(Throwable.class, () -> mRecordingService.onStopped());
+        assertThrows(Throwable.class, () -> mRecordingService.onStopped(StopReason.STOP_UNKNOWN));
 
         verify(mScreenMediaRecorder).release();
     }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
index aceea90..ade5941 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.screenrecord.data.repository
 
+import android.media.projection.StopReason
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -31,6 +32,7 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
 import org.mockito.kotlin.mock
 import org.mockito.kotlin.verify
 import org.mockito.kotlin.whenever
@@ -126,8 +128,8 @@
     @Test
     fun stopRecording_invokesController() =
         testScope.runTest {
-            underTest.stopRecording()
+            underTest.stopRecording(StopReason.STOP_PRIVACY_CHIP)
 
-            verify(recordingController).stopRecording()
+            verify(recordingController).stopRecording(eq(StopReason.STOP_PRIVACY_CHIP))
         }
 }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt
new file mode 100644
index 0000000..44a8518
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt
@@ -0,0 +1,684 @@
+/*
+ * Copyright (C) 2019 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.statusbar.notification.headsup
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.kosmos.KosmosJavaAdapter
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.settings.FakeGlobalSettings
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.eq
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWithLooper
+@RunWith(ParameterizedAndroidJunit4::class)
+// TODO(b/378142453): Merge this with HeadsUpManagerImplTest.
+open class HeadsUpManagerImplOldTest(flags: FlagsParameterization?) : SysuiTestCase() {
+    protected var mKosmos: KosmosJavaAdapter = KosmosJavaAdapter(this)
+
+    @JvmField @Rule var rule: MockitoRule = MockitoJUnit.rule()
+
+    private val mUiEventLoggerFake = UiEventLoggerFake()
+
+    private val mLogger: HeadsUpManagerLogger = Mockito.spy(HeadsUpManagerLogger(logcatLogBuffer()))
+
+    @Mock private val mBgHandler: Handler? = null
+
+    @Mock private val dumpManager: DumpManager? = null
+
+    @Mock private val mShadeInteractor: ShadeInteractor? = null
+    private var mAvalancheController: AvalancheController? = null
+
+    @Mock private val mAccessibilityMgr: AccessibilityManagerWrapper? = null
+
+    protected val mGlobalSettings: FakeGlobalSettings = FakeGlobalSettings()
+    protected val mSystemClock: FakeSystemClock = FakeSystemClock()
+    protected val mExecutor: FakeExecutor = FakeExecutor(mSystemClock)
+
+    @Mock protected var mRow: ExpandableNotificationRow? = null
+
+    private fun createHeadsUpManager(): HeadsUpManagerImpl {
+        return TestableHeadsUpManager(
+            mContext,
+            mLogger,
+            mKosmos.statusBarStateController,
+            mKosmos.keyguardBypassController,
+            GroupMembershipManagerImpl(),
+            mKosmos.visualStabilityProvider,
+            mKosmos.configurationController,
+            mExecutor,
+            mGlobalSettings,
+            mSystemClock,
+            mAccessibilityMgr,
+            mUiEventLoggerFake,
+            JavaAdapter(mKosmos.testScope),
+            mShadeInteractor,
+            mAvalancheController,
+        )
+    }
+
+    private fun createStickyEntry(id: Int): NotificationEntry {
+        val notif =
+            Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setFullScreenIntent(
+                    Mockito.mock(PendingIntent::class.java), /* highPriority */
+                    true,
+                )
+                .build()
+        return HeadsUpManagerTestUtil.createEntry(id, notif)
+    }
+
+    private fun createStickyForSomeTimeEntry(id: Int): NotificationEntry {
+        val notif =
+            Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setFlag(Notification.FLAG_FSI_REQUESTED_BUT_DENIED, true)
+                .build()
+        return HeadsUpManagerTestUtil.createEntry(id, notif)
+    }
+
+    private fun useAccessibilityTimeout(use: Boolean) {
+        if (use) {
+            Mockito.doReturn(TEST_A11Y_AUTO_DISMISS_TIME)
+                .`when`(mAccessibilityMgr!!)
+                .getRecommendedTimeoutMillis(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
+        } else {
+            Mockito.`when`(
+                    mAccessibilityMgr!!.getRecommendedTimeoutMillis(
+                        ArgumentMatchers.anyInt(),
+                        ArgumentMatchers.anyInt(),
+                    )
+                )
+                .then { i: InvocationOnMock -> i.getArgument(0) }
+        }
+    }
+
+    init {
+        mSetFlagsRule.setFlagsParameterization(flags!!)
+    }
+
+    @Throws(Exception::class)
+    override fun SysuiSetup() {
+        super.SysuiSetup()
+        mAvalancheController =
+            AvalancheController(dumpManager!!, mUiEventLoggerFake, mLogger, mBgHandler!!)
+        Mockito.`when`(mShadeInteractor!!.isAnyExpanded).thenReturn(MutableStateFlow(true))
+        Mockito.`when`(mKosmos.keyguardBypassController.bypassEnabled).thenReturn(false)
+    }
+
+    @Test
+    fun testHasNotifications_headsUpManagerMapNotEmpty_true() {
+        val bhum = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        bhum.showNotification(entry)
+
+        Truth.assertThat(bhum.mHeadsUpEntryMap).isNotEmpty()
+        Truth.assertThat(bhum.hasNotifications()).isTrue()
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testHasNotifications_avalancheMapNotEmpty_true() {
+        val bhum = createHeadsUpManager()
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
+        mAvalancheController!!.addToNext(headsUpEntry) {}
+
+        Truth.assertThat(mAvalancheController!!.getWaitingEntryList()).isNotEmpty()
+        Truth.assertThat(bhum.hasNotifications()).isTrue()
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testHasNotifications_false() {
+        val bhum = createHeadsUpManager()
+        Truth.assertThat(bhum.mHeadsUpEntryMap).isEmpty()
+        Truth.assertThat(mAvalancheController!!.getWaitingEntryList()).isEmpty()
+        Truth.assertThat(bhum.hasNotifications()).isFalse()
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testGetHeadsUpEntryList_includesAvalancheEntryList() {
+        val bhum = createHeadsUpManager()
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
+        mAvalancheController!!.addToNext(headsUpEntry) {}
+
+        Truth.assertThat(bhum.headsUpEntryList).contains(headsUpEntry)
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testGetHeadsUpEntry_returnsAvalancheEntry() {
+        val bhum = createHeadsUpManager()
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
+        mAvalancheController!!.addToNext(headsUpEntry) {}
+
+        Truth.assertThat(bhum.getHeadsUpEntry(notifEntry.key)).isEqualTo(headsUpEntry)
+    }
+
+    @Test
+    fun testShowNotification_addsEntry() {
+        val alm = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        alm.showNotification(entry)
+
+        assertThat(alm.isHeadsUpEntry(entry.key)).isTrue()
+        assertThat(alm.hasNotifications()).isTrue()
+        assertThat(alm.getEntry(entry.key)).isEqualTo(entry)
+    }
+
+    @Test
+    fun testShowNotification_autoDismisses() {
+        val alm = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        alm.showNotification(entry)
+        mSystemClock.advanceTime((TEST_AUTO_DISMISS_TIME * 3 / 2).toLong())
+
+        assertThat(alm.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testRemoveNotification_removeDeferred() {
+        val alm = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        alm.showNotification(entry)
+
+        val removedImmediately =
+            alm.removeNotification(entry.key, /* releaseImmediately= */ false, "removeDeferred")
+        assertThat(removedImmediately).isFalse()
+        assertThat(alm.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testRemoveNotification_forceRemove() {
+        val alm = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        alm.showNotification(entry)
+
+        val removedImmediately =
+            alm.removeNotification(entry.key, /* releaseImmediately= */ true, "forceRemove")
+        assertThat(removedImmediately).isTrue()
+        assertThat(alm.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testReleaseAllImmediately() {
+        val alm = createHeadsUpManager()
+        for (i in 0 until TEST_NUM_NOTIFICATIONS) {
+            val entry = HeadsUpManagerTestUtil.createEntry(i, mContext)
+            entry.row = mRow
+            alm.showNotification(entry)
+        }
+
+        alm.releaseAllImmediately()
+
+        assertThat(alm.allEntries.count()).isEqualTo(0)
+    }
+
+    @Test
+    fun testCanRemoveImmediately_notShownLongEnough() {
+        val alm = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        alm.showNotification(entry)
+
+        // The entry has just been added so we should not remove immediately.
+        assertThat(alm.canRemoveImmediately(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testHunRemovedLogging() {
+        val hum = createHeadsUpManager()
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        val headsUpEntry = Mockito.mock(HeadsUpEntry::class.java)
+        Mockito.`when`(headsUpEntry.pinnedStatus)
+            .thenReturn(MutableStateFlow(PinnedStatus.NotPinned))
+        headsUpEntry.mEntry = notifEntry
+
+        hum.onEntryRemoved(headsUpEntry, "test")
+
+        Mockito.verify(mLogger, Mockito.times(1)).logNotificationActuallyRemoved(eq(notifEntry))
+    }
+
+    @Test
+    fun testShowNotification_autoDismissesIncludingTouchAcceptanceDelay() {
+        val hum = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(false)
+
+        hum.showNotification(entry)
+        mSystemClock.advanceTime((TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME).toLong())
+
+        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testShowNotification_autoDismissesWithDefaultTimeout() {
+        val hum = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(false)
+
+        hum.showNotification(entry)
+        mSystemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME +
+                    (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+                .toLong()
+        )
+
+        assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testShowNotification_stickyForSomeTime_autoDismissesWithStickyTimeout() {
+        val hum = createHeadsUpManager()
+        val entry = createStickyForSomeTimeEntry(/* id= */ 0)
+        useAccessibilityTimeout(false)
+
+        hum.showNotification(entry)
+        mSystemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME +
+                    (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2)
+                .toLong()
+        )
+
+        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testShowNotification_sticky_neverAutoDismisses() {
+        val hum = createHeadsUpManager()
+        val entry = createStickyEntry(/* id= */ 0)
+        useAccessibilityTimeout(false)
+
+        hum.showNotification(entry)
+        mSystemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME).toLong()
+        )
+
+        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testShowNotification_autoDismissesWithAccessibilityTimeout() {
+        val hum = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(true)
+
+        hum.showNotification(entry)
+        mSystemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME +
+                    (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+                .toLong()
+        )
+
+        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testShowNotification_stickyForSomeTime_autoDismissesWithAccessibilityTimeout() {
+        val hum = createHeadsUpManager()
+        val entry = createStickyForSomeTimeEntry(/* id= */ 0)
+        useAccessibilityTimeout(true)
+
+        hum.showNotification(entry)
+        mSystemClock.advanceTime(
+            (TEST_TOUCH_ACCEPTANCE_TIME +
+                    (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+                .toLong()
+        )
+
+        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+    }
+
+    @Test
+    fun testRemoveNotification_beforeMinimumDisplayTime() {
+        val hum = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(false)
+
+        hum.showNotification(entry)
+
+        val removedImmediately =
+            hum.removeNotification(
+                entry.key,
+                /* releaseImmediately = */ false,
+                "beforeMinimumDisplayTime",
+            )
+        assertThat(removedImmediately).isFalse()
+        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+
+        mSystemClock.advanceTime(
+            ((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+        )
+
+        assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testRemoveNotification_afterMinimumDisplayTime() {
+        val hum = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        useAccessibilityTimeout(false)
+
+        hum.showNotification(entry)
+        mSystemClock.advanceTime(
+            ((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+        )
+
+        assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+
+        val removedImmediately =
+            hum.removeNotification(
+                entry.key,
+                /* releaseImmediately = */ false,
+                "afterMinimumDisplayTime",
+            )
+        assertThat(removedImmediately).isTrue()
+        assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testRemoveNotification_releaseImmediately() {
+        val hum = createHeadsUpManager()
+        val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        hum.showNotification(entry)
+
+        val removedImmediately =
+            hum.removeNotification(
+                entry.key,
+                /* releaseImmediately = */ true,
+                "afterMinimumDisplayTime",
+            )
+        assertThat(removedImmediately).isTrue()
+        assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testIsSticky_rowPinnedAndExpanded_true() {
+        val hum = createHeadsUpManager()
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+        Mockito.`when`(mRow!!.isPinned).thenReturn(true)
+        notifEntry.row = mRow
+
+        hum.showNotification(notifEntry)
+
+        val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
+        headsUpEntry!!.setExpanded(true)
+
+        assertThat(hum.isSticky(notifEntry.key)).isTrue()
+    }
+
+    @Test
+    fun testIsSticky_remoteInputActive_true() {
+        val hum = createHeadsUpManager()
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        hum.showNotification(notifEntry)
+
+        val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
+        headsUpEntry!!.mRemoteInputActive = true
+
+        assertThat(hum.isSticky(notifEntry.key)).isTrue()
+    }
+
+    @Test
+    fun testIsSticky_hasFullScreenIntent_true() {
+        val hum = createHeadsUpManager()
+        val notifEntry = HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+
+        hum.showNotification(notifEntry)
+
+        assertThat(hum.isSticky(notifEntry.key)).isTrue()
+    }
+
+    @Test
+    fun testIsSticky_stickyForSomeTime_false() {
+        val hum = createHeadsUpManager()
+        val entry = createStickyForSomeTimeEntry(/* id= */ 0)
+
+        hum.showNotification(entry)
+
+        assertThat(hum.isSticky(entry.key)).isFalse()
+    }
+
+    @Test
+    fun testIsSticky_false() {
+        val hum = createHeadsUpManager()
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        hum.showNotification(notifEntry)
+
+        val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
+        headsUpEntry!!.setExpanded(false)
+        headsUpEntry.mRemoteInputActive = false
+
+        assertThat(hum.isSticky(notifEntry.key)).isFalse()
+    }
+
+    @Test
+    fun testCompareTo_withNullEntries() {
+        val hum = createHeadsUpManager()
+        val alertEntry = NotificationEntryBuilder().setTag("alert").build()
+
+        hum.showNotification(alertEntry)
+
+        assertThat(hum.compare(alertEntry, null)).isLessThan(0)
+        assertThat(hum.compare(null, alertEntry)).isGreaterThan(0)
+        assertThat(hum.compare(null, null)).isEqualTo(0)
+    }
+
+    @Test
+    fun testCompareTo_withNonAlertEntries() {
+        val hum = createHeadsUpManager()
+
+        val nonAlertEntry1 = NotificationEntryBuilder().setTag("nae1").build()
+        val nonAlertEntry2 = NotificationEntryBuilder().setTag("nae2").build()
+        val alertEntry = NotificationEntryBuilder().setTag("alert").build()
+        hum.showNotification(alertEntry)
+
+        assertThat(hum.compare(alertEntry, nonAlertEntry1)).isLessThan(0)
+        assertThat(hum.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0)
+        assertThat(hum.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0)
+    }
+
+    @Test
+    fun testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
+        val hum = createHeadsUpManager()
+
+        val ongoingCall =
+            hum.HeadsUpEntry(
+                NotificationEntryBuilder()
+                    .setSbn(
+                        HeadsUpManagerTestUtil.createSbn(
+                            /* id = */ 0,
+                            Notification.Builder(mContext, "")
+                                .setCategory(Notification.CATEGORY_CALL)
+                                .setOngoing(true),
+                        )
+                    )
+                    .build()
+            )
+
+        val activeRemoteInput =
+            hum.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
+        activeRemoteInput.mRemoteInputActive = true
+
+        assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0)
+        assertThat(activeRemoteInput.compareTo(ongoingCall)).isGreaterThan(0)
+    }
+
+    @Test
+    fun testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
+        val hum = createHeadsUpManager()
+
+        val person = Person.Builder().setName("person").build()
+        val intent = Mockito.mock(PendingIntent::class.java)
+        val incomingCall =
+            hum.HeadsUpEntry(
+                NotificationEntryBuilder()
+                    .setSbn(
+                        HeadsUpManagerTestUtil.createSbn(
+                            /* id = */ 0,
+                            Notification.Builder(mContext, "")
+                                .setStyle(
+                                    Notification.CallStyle.forIncomingCall(person, intent, intent)
+                                ),
+                        )
+                    )
+                    .build()
+            )
+
+        val activeRemoteInput =
+            hum.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
+        activeRemoteInput.mRemoteInputActive = true
+
+        assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0)
+        assertThat(activeRemoteInput.compareTo(incomingCall)).isGreaterThan(0)
+    }
+
+    @Test
+    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testPinEntry_logsPeek_throttleEnabled() {
+        val hum = createHeadsUpManager()
+
+        // Needs full screen intent in order to be pinned
+        val entryToPin =
+            hum.HeadsUpEntry(
+                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+            )
+
+        // Note: the standard way to show a notification would be calling showNotification rather
+        // than onAlertEntryAdded. However, in practice showNotification in effect adds
+        // the notification and then updates it; in order to not log twice, the entry needs
+        // to have a functional ExpandableNotificationRow that can keep track of whether it's
+        // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
+        hum.onEntryAdded(entryToPin)
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(2)
+        assertThat(
+            AvalancheController.ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN.getId(),
+        ).isEqualTo(mUiEventLoggerFake.eventId(0))
+        assertThat(
+            HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id,
+        ).isEqualTo(mUiEventLoggerFake.eventId(1))
+    }
+
+    @Test
+    @DisableFlags(NotificationThrottleHun.FLAG_NAME)
+    fun testPinEntry_logsPeek_throttleDisabled() {
+        val hum = createHeadsUpManager()
+
+        // Needs full screen intent in order to be pinned
+        val entryToPin =
+            hum.HeadsUpEntry(
+                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+            )
+
+        // Note: the standard way to show a notification would be calling showNotification rather
+        // than onAlertEntryAdded. However, in practice showNotification in effect adds
+        // the notification and then updates it; in order to not log twice, the entry needs
+        // to have a functional ExpandableNotificationRow that can keep track of whether it's
+        // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
+        hum.onEntryAdded(entryToPin)
+
+        assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1)
+        assertThat(
+            HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id,
+        ).isEqualTo(mUiEventLoggerFake.eventId(0))
+    }
+
+    @Test
+    fun testSetUserActionMayIndirectlyRemove() {
+        val hum = createHeadsUpManager()
+        val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+        hum.showNotification(notifEntry)
+
+        assertThat(hum.canRemoveImmediately(notifEntry.key)).isFalse()
+
+        hum.setUserActionMayIndirectlyRemove(notifEntry)
+
+        assertThat(hum.canRemoveImmediately(notifEntry.key)).isTrue()
+    }
+
+    companion object {
+        const val TEST_TOUCH_ACCEPTANCE_TIME: Int = 200
+        const val TEST_A11Y_AUTO_DISMISS_TIME: Int = 1000
+
+        const val TEST_MINIMUM_DISPLAY_TIME: Int = 400
+        const val TEST_AUTO_DISMISS_TIME: Int = 600
+        const val TEST_STICKY_AUTO_DISMISS_TIME: Int = 800
+
+        // Number of notifications to use in tests requiring multiple notifications
+        private const val TEST_NUM_NOTIFICATIONS = 4
+
+        init {
+            Truth.assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME)
+            Truth.assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME)
+            Truth.assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME)
+        }
+
+        @get:Parameters(name = "{0}")
+        @JvmStatic
+        val flags: List<FlagsParameterization>
+            get() = FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME)
+    }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.java
deleted file mode 100644
index 01f78cb..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.java
+++ /dev/null
@@ -1,664 +0,0 @@
-/*
- * Copyright (C) 2019 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.statusbar.notification.headsup;
-
-import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED;
-
-import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Person;
-import android.os.Handler;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
-import android.platform.test.flag.junit.FlagsParameterization;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.logging.testing.UiEventLoggerFake;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.kosmos.KosmosJavaAdapter;
-import com.android.systemui.res.R;
-import com.android.systemui.shade.domain.interactor.ShadeInteractor;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.kotlin.JavaAdapter;
-import com.android.systemui.util.settings.FakeGlobalSettings;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import kotlinx.coroutines.flow.StateFlowKt;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
-import java.util.List;
-
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(ParameterizedAndroidJunit4.class)
-// TODO(b/378142453): Merge this with HeadsUpManagerPhoneTest.
-public class HeadsUpManagerImplTest extends SysuiTestCase {
-    protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
-
-    @Rule
-    public MockitoRule rule = MockitoJUnit.rule();
-
-    static final int TEST_TOUCH_ACCEPTANCE_TIME = 200;
-    static final int TEST_A11Y_AUTO_DISMISS_TIME = 1_000;
-
-    private UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
-
-    private final HeadsUpManagerLogger mLogger = spy(new HeadsUpManagerLogger(logcatLogBuffer()));
-    @Mock private Handler mBgHandler;
-    @Mock private DumpManager dumpManager;
-    @Mock private ShadeInteractor mShadeInteractor;
-    private AvalancheController mAvalancheController;
-
-    @Mock private AccessibilityManagerWrapper mAccessibilityMgr;
-
-    protected static final int TEST_MINIMUM_DISPLAY_TIME = 400;
-    protected static final int TEST_AUTO_DISMISS_TIME = 600;
-    protected static final int TEST_STICKY_AUTO_DISMISS_TIME = 800;
-    // Number of notifications to use in tests requiring multiple notifications
-    private static final int TEST_NUM_NOTIFICATIONS = 4;
-
-    protected final FakeGlobalSettings mGlobalSettings = new FakeGlobalSettings();
-    protected final FakeSystemClock mSystemClock = new FakeSystemClock();
-    protected final FakeExecutor mExecutor = new FakeExecutor(mSystemClock);
-
-    @Mock protected ExpandableNotificationRow mRow;
-
-    static {
-        assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME);
-        assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME);
-        assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME);
-    }
-
-    private HeadsUpManagerImpl createHeadsUpManager() {
-        return new TestableHeadsUpManager(
-                mContext,
-                mLogger,
-                mKosmos.getStatusBarStateController(),
-                mKosmos.getKeyguardBypassController(),
-                new GroupMembershipManagerImpl(),
-                mKosmos.getVisualStabilityProvider(),
-                mKosmos.getConfigurationController(),
-                mExecutor,
-                mGlobalSettings,
-                mSystemClock,
-                mAccessibilityMgr,
-                mUiEventLoggerFake,
-                new JavaAdapter(mKosmos.getTestScope()),
-                mShadeInteractor,
-                mAvalancheController);
-    }
-
-    private NotificationEntry createStickyEntry(int id) {
-        final Notification notif = new Notification.Builder(mContext, "")
-                .setSmallIcon(R.drawable.ic_person)
-                .setFullScreenIntent(mock(PendingIntent.class), /* highPriority */ true)
-                .build();
-        return HeadsUpManagerTestUtil.createEntry(id, notif);
-    }
-
-    private NotificationEntry createStickyForSomeTimeEntry(int id) {
-        final Notification notif = new Notification.Builder(mContext, "")
-                .setSmallIcon(R.drawable.ic_person)
-                .setFlag(FLAG_FSI_REQUESTED_BUT_DENIED, true)
-                .build();
-        return HeadsUpManagerTestUtil.createEntry(id, notif);
-    }
-
-    private void useAccessibilityTimeout(boolean use) {
-        if (use) {
-            doReturn(TEST_A11Y_AUTO_DISMISS_TIME).when(mAccessibilityMgr)
-                    .getRecommendedTimeoutMillis(anyInt(), anyInt());
-        } else {
-            when(mAccessibilityMgr.getRecommendedTimeoutMillis(anyInt(), anyInt())).then(
-                    i -> i.getArgument(0));
-        }
-    }
-
-    @Parameters(name = "{0}")
-    public static List<FlagsParameterization> getFlags() {
-        return FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME);
-    }
-
-    public HeadsUpManagerImplTest(FlagsParameterization flags) {
-        mSetFlagsRule.setFlagsParameterization(flags);
-    }
-
-    @Override
-    public void SysuiSetup() throws Exception {
-        super.SysuiSetup();
-        mAvalancheController = new AvalancheController(dumpManager, mUiEventLoggerFake, mLogger,
-                mBgHandler);
-        when(mShadeInteractor.isAnyExpanded()).thenReturn(MutableStateFlow(true));
-        when(mKosmos.getKeyguardBypassController().getBypassEnabled()).thenReturn(false);
-    }
-
-    @Test
-    public void testHasNotifications_headsUpManagerMapNotEmpty_true() {
-        final HeadsUpManagerImpl bhum = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-        bhum.showNotification(entry);
-
-        assertThat(bhum.mHeadsUpEntryMap).isNotEmpty();
-        assertThat(bhum.hasNotifications()).isTrue();
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    public void testHasNotifications_avalancheMapNotEmpty_true() {
-        final HeadsUpManagerImpl bhum = createHeadsUpManager();
-        final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
-                mContext);
-        final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = bhum.createHeadsUpEntry(notifEntry);
-        mAvalancheController.addToNext(headsUpEntry, () -> {});
-
-        assertThat(mAvalancheController.getWaitingEntryList()).isNotEmpty();
-        assertThat(bhum.hasNotifications()).isTrue();
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    public void testHasNotifications_false() {
-        final HeadsUpManagerImpl bhum = createHeadsUpManager();
-        assertThat(bhum.mHeadsUpEntryMap).isEmpty();
-        assertThat(mAvalancheController.getWaitingEntryList()).isEmpty();
-        assertThat(bhum.hasNotifications()).isFalse();
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    public void testGetHeadsUpEntryList_includesAvalancheEntryList() {
-        final HeadsUpManagerImpl bhum = createHeadsUpManager();
-        final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
-                mContext);
-        final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = bhum.createHeadsUpEntry(notifEntry);
-        mAvalancheController.addToNext(headsUpEntry, () -> {});
-
-        assertThat(bhum.getHeadsUpEntryList()).contains(headsUpEntry);
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    public void testGetHeadsUpEntry_returnsAvalancheEntry() {
-        final HeadsUpManagerImpl bhum = createHeadsUpManager();
-        final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
-                mContext);
-        final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = bhum.createHeadsUpEntry(notifEntry);
-        mAvalancheController.addToNext(headsUpEntry, () -> {});
-
-        assertThat(bhum.getHeadsUpEntry(notifEntry.getKey())).isEqualTo(headsUpEntry);
-    }
-
-    @Test
-    public void testShowNotification_addsEntry() {
-        final HeadsUpManagerImpl alm = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        alm.showNotification(entry);
-
-        assertTrue(alm.isHeadsUpEntry(entry.getKey()));
-        assertTrue(alm.hasNotifications());
-        assertEquals(entry, alm.getEntry(entry.getKey()));
-    }
-
-    @Test
-    public void testShowNotification_autoDismisses() {
-        final HeadsUpManagerImpl alm = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        alm.showNotification(entry);
-        mSystemClock.advanceTime(TEST_AUTO_DISMISS_TIME * 3 / 2);
-
-        assertFalse(alm.isHeadsUpEntry(entry.getKey()));
-    }
-
-    @Test
-    public void testRemoveNotification_removeDeferred() {
-        final HeadsUpManagerImpl alm = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        alm.showNotification(entry);
-
-        final boolean removedImmediately = alm.removeNotification(
-                entry.getKey(), /* releaseImmediately = */ false, "removeDeferred");
-        assertFalse(removedImmediately);
-        assertTrue(alm.isHeadsUpEntry(entry.getKey()));
-    }
-
-    @Test
-    public void testRemoveNotification_forceRemove() {
-        final HeadsUpManagerImpl alm = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        alm.showNotification(entry);
-
-        final boolean removedImmediately = alm.removeNotification(
-                entry.getKey(), /* releaseImmediately = */ true, "forceRemove");
-        assertTrue(removedImmediately);
-        assertFalse(alm.isHeadsUpEntry(entry.getKey()));
-    }
-
-    @Test
-    public void testReleaseAllImmediately() {
-        final HeadsUpManagerImpl alm = createHeadsUpManager();
-        for (int i = 0; i < TEST_NUM_NOTIFICATIONS; i++) {
-            final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(i, mContext);
-            entry.setRow(mRow);
-            alm.showNotification(entry);
-        }
-
-        alm.releaseAllImmediately();
-
-        assertEquals(0, alm.getAllEntries().count());
-    }
-
-    @Test
-    public void testCanRemoveImmediately_notShownLongEnough() {
-        final HeadsUpManagerImpl alm = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        alm.showNotification(entry);
-
-        // The entry has just been added so we should not remove immediately.
-        assertFalse(alm.canRemoveImmediately(entry.getKey()));
-    }
-
-    @Test
-    public void testHunRemovedLogging() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
-                mContext);
-        final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = mock(
-                HeadsUpManagerImpl.HeadsUpEntry.class);
-        when(headsUpEntry.getPinnedStatus())
-                .thenReturn(StateFlowKt.MutableStateFlow(PinnedStatus.NotPinned));
-        headsUpEntry.mEntry = notifEntry;
-
-        hum.onEntryRemoved(headsUpEntry, "test");
-
-        verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(notifEntry));
-    }
-
-
-    @Test
-    public void testShowNotification_autoDismissesIncludingTouchAcceptanceDelay() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-        useAccessibilityTimeout(false);
-
-        hum.showNotification(entry);
-        mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME);
-
-        assertTrue(hum.isHeadsUpEntry(entry.getKey()));
-    }
-
-
-    @Test
-    public void testShowNotification_autoDismissesWithDefaultTimeout() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-        useAccessibilityTimeout(false);
-
-        hum.showNotification(entry);
-        mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME
-                + (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2);
-
-        assertFalse(hum.isHeadsUpEntry(entry.getKey()));
-    }
-
-
-    @Test
-    public void testShowNotification_stickyForSomeTime_autoDismissesWithStickyTimeout() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = createStickyForSomeTimeEntry(/* id = */ 0);
-        useAccessibilityTimeout(false);
-
-        hum.showNotification(entry);
-        mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME
-                + (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2);
-
-        assertTrue(hum.isHeadsUpEntry(entry.getKey()));
-    }
-
-
-    @Test
-    public void testShowNotification_sticky_neverAutoDismisses() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = createStickyEntry(/* id = */ 0);
-        useAccessibilityTimeout(false);
-
-        hum.showNotification(entry);
-        mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME);
-
-        assertTrue(hum.isHeadsUpEntry(entry.getKey()));
-    }
-
-
-    @Test
-    public void testShowNotification_autoDismissesWithAccessibilityTimeout() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-        useAccessibilityTimeout(true);
-
-        hum.showNotification(entry);
-        mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME
-                + (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2);
-
-        assertTrue(hum.isHeadsUpEntry(entry.getKey()));
-    }
-
-
-    @Test
-    public void testShowNotification_stickyForSomeTime_autoDismissesWithAccessibilityTimeout() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = createStickyForSomeTimeEntry(/* id = */ 0);
-        useAccessibilityTimeout(true);
-
-        hum.showNotification(entry);
-        mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME
-                + (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2);
-
-        assertTrue(hum.isHeadsUpEntry(entry.getKey()));
-    }
-
-
-    @Test
-    public void testRemoveNotification_beforeMinimumDisplayTime() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-        useAccessibilityTimeout(false);
-
-        hum.showNotification(entry);
-
-        final boolean removedImmediately = hum.removeNotification(
-                entry.getKey(), /* releaseImmediately = */ false, "beforeMinimumDisplayTime");
-        assertFalse(removedImmediately);
-        assertTrue(hum.isHeadsUpEntry(entry.getKey()));
-
-        mSystemClock.advanceTime((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2);
-
-        assertFalse(hum.isHeadsUpEntry(entry.getKey()));
-    }
-
-
-    @Test
-    public void testRemoveNotification_afterMinimumDisplayTime() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-        useAccessibilityTimeout(false);
-
-        hum.showNotification(entry);
-        mSystemClock.advanceTime((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2);
-
-        assertTrue(hum.isHeadsUpEntry(entry.getKey()));
-
-        final boolean removedImmediately = hum.removeNotification(
-                entry.getKey(), /* releaseImmediately = */ false, "afterMinimumDisplayTime");
-        assertTrue(removedImmediately);
-        assertFalse(hum.isHeadsUpEntry(entry.getKey()));
-    }
-
-
-    @Test
-    public void testRemoveNotification_releaseImmediately() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
-        hum.showNotification(entry);
-
-        final boolean removedImmediately = hum.removeNotification(
-                entry.getKey(), /* releaseImmediately = */ true, "afterMinimumDisplayTime");
-        assertTrue(removedImmediately);
-        assertFalse(hum.isHeadsUpEntry(entry.getKey()));
-    }
-
-
-    @Test
-    public void testIsSticky_rowPinnedAndExpanded_true() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
-                mContext);
-        when(mRow.isPinned()).thenReturn(true);
-        notifEntry.setRow(mRow);
-
-        hum.showNotification(notifEntry);
-
-        final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(
-                notifEntry.getKey());
-        headsUpEntry.setExpanded(true);
-
-        assertTrue(hum.isSticky(notifEntry.getKey()));
-    }
-
-    @Test
-    public void testIsSticky_remoteInputActive_true() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
-                mContext);
-
-        hum.showNotification(notifEntry);
-
-        final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(
-                notifEntry.getKey());
-        headsUpEntry.mRemoteInputActive = true;
-
-        assertTrue(hum.isSticky(notifEntry.getKey()));
-    }
-
-    @Test
-    public void testIsSticky_hasFullScreenIntent_true() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry notifEntry =
-                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext);
-
-        hum.showNotification(notifEntry);
-
-        assertTrue(hum.isSticky(notifEntry.getKey()));
-    }
-
-
-    @Test
-    public void testIsSticky_stickyForSomeTime_false() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry entry = createStickyForSomeTimeEntry(/* id = */ 0);
-
-        hum.showNotification(entry);
-
-        assertFalse(hum.isSticky(entry.getKey()));
-    }
-
-
-    @Test
-    public void testIsSticky_false() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
-                mContext);
-
-        hum.showNotification(notifEntry);
-
-        final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(
-                notifEntry.getKey());
-        headsUpEntry.setExpanded(false);
-        headsUpEntry.mRemoteInputActive = false;
-
-        assertFalse(hum.isSticky(notifEntry.getKey()));
-    }
-
-    @Test
-    public void testCompareTo_withNullEntries() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry alertEntry = new NotificationEntryBuilder().setTag("alert").build();
-
-        hum.showNotification(alertEntry);
-
-        assertThat(hum.compare(alertEntry, null)).isLessThan(0);
-        assertThat(hum.compare(null, alertEntry)).isGreaterThan(0);
-        assertThat(hum.compare(null, null)).isEqualTo(0);
-    }
-
-    @Test
-    public void testCompareTo_withNonAlertEntries() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-
-        final NotificationEntry nonAlertEntry1 = new NotificationEntryBuilder().setTag(
-                "nae1").build();
-        final NotificationEntry nonAlertEntry2 = new NotificationEntryBuilder().setTag(
-                "nae2").build();
-        final NotificationEntry alertEntry = new NotificationEntryBuilder().setTag("alert").build();
-        hum.showNotification(alertEntry);
-
-        assertThat(hum.compare(alertEntry, nonAlertEntry1)).isLessThan(0);
-        assertThat(hum.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0);
-        assertThat(hum.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0);
-    }
-
-    @Test
-    public void testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-
-        final HeadsUpManagerImpl.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry(
-                new NotificationEntryBuilder()
-                        .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
-                                new Notification.Builder(mContext, "")
-                                        .setCategory(Notification.CATEGORY_CALL)
-                                        .setOngoing(true)))
-                        .build());
-
-        final HeadsUpManagerImpl.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(
-                HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
-        activeRemoteInput.mRemoteInputActive = true;
-
-        assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0);
-        assertThat(activeRemoteInput.compareTo(ongoingCall)).isGreaterThan(0);
-    }
-
-    @Test
-    public void testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-
-        final Person person = new Person.Builder().setName("person").build();
-        final PendingIntent intent = mock(PendingIntent.class);
-        final HeadsUpManagerImpl.HeadsUpEntry incomingCall = hum.new HeadsUpEntry(
-                new NotificationEntryBuilder()
-                        .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
-                                new Notification.Builder(mContext, "")
-                                        .setStyle(Notification.CallStyle
-                                                .forIncomingCall(person, intent, intent))))
-                        .build());
-
-        final HeadsUpManagerImpl.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(
-                HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
-        activeRemoteInput.mRemoteInputActive = true;
-
-        assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0);
-        assertThat(activeRemoteInput.compareTo(incomingCall)).isGreaterThan(0);
-    }
-
-    @Test
-    @EnableFlags(NotificationThrottleHun.FLAG_NAME)
-    public void testPinEntry_logsPeek_throttleEnabled() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-
-        // Needs full screen intent in order to be pinned
-        final HeadsUpManagerImpl.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(
-                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext));
-
-        // Note: the standard way to show a notification would be calling showNotification rather
-        // than onAlertEntryAdded. However, in practice showNotification in effect adds
-        // the notification and then updates it; in order to not log twice, the entry needs
-        // to have a functional ExpandableNotificationRow that can keep track of whether it's
-        // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
-        hum.onEntryAdded(entryToPin);
-
-        assertEquals(2, mUiEventLoggerFake.numLogs());
-        assertEquals(AvalancheController.ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN.getId(),
-                mUiEventLoggerFake.eventId(0));
-        assertEquals(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.getId(),
-                mUiEventLoggerFake.eventId(1));
-    }
-
-    @Test
-    @DisableFlags(NotificationThrottleHun.FLAG_NAME)
-    public void testPinEntry_logsPeek_throttleDisabled() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-
-        // Needs full screen intent in order to be pinned
-        final HeadsUpManagerImpl.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(
-                HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext));
-
-        // Note: the standard way to show a notification would be calling showNotification rather
-        // than onAlertEntryAdded. However, in practice showNotification in effect adds
-        // the notification and then updates it; in order to not log twice, the entry needs
-        // to have a functional ExpandableNotificationRow that can keep track of whether it's
-        // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
-        hum.onEntryAdded(entryToPin);
-
-        assertEquals(1, mUiEventLoggerFake.numLogs());
-        assertEquals(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.getId(),
-                mUiEventLoggerFake.eventId(0));
-    }
-
-    @Test
-    public void testSetUserActionMayIndirectlyRemove() {
-        final HeadsUpManagerImpl hum = createHeadsUpManager();
-        final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
-                mContext);
-
-        hum.showNotification(notifEntry);
-
-        assertFalse(hum.canRemoveImmediately(notifEntry.getKey()));
-
-        hum.setUserActionMayIndirectlyRemove(notifEntry);
-
-        assertTrue(hum.canRemoveImmediately(notifEntry.getKey()));
-    }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
similarity index 98%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerPhoneTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index 35d8253..65d282f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -59,7 +59,7 @@
 @SmallTest
 @RunWith(ParameterizedAndroidJunit4::class)
 @RunWithLooper
-class HeadsUpManagerPhoneTest(flags: FlagsParameterization) : HeadsUpManagerImplTest(flags) {
+class HeadsUpManagerImplTest(flags: FlagsParameterization) : HeadsUpManagerImplOldTest(flags) {
 
     private val mHeadsUpManagerLogger = HeadsUpManagerLogger(logcatLogBuffer())
 
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/TestableHeadsUpManager.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/TestableHeadsUpManager.java
index 2b077ed..7ded1a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/TestableHeadsUpManager.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/TestableHeadsUpManager.java
@@ -78,10 +78,10 @@
                 shadeInteractor,
                 avalancheController);
 
-        mTouchAcceptanceDelay = HeadsUpManagerImplTest.TEST_TOUCH_ACCEPTANCE_TIME;
-        mMinimumDisplayTime = HeadsUpManagerImplTest.TEST_MINIMUM_DISPLAY_TIME;
-        mAutoDismissTime = HeadsUpManagerImplTest.TEST_AUTO_DISMISS_TIME;
-        mStickyForSomeTimeAutoDismissTime = HeadsUpManagerImplTest.TEST_STICKY_AUTO_DISMISS_TIME;
+        mTouchAcceptanceDelay = HeadsUpManagerImplOldTest.TEST_TOUCH_ACCEPTANCE_TIME;
+        mMinimumDisplayTime = HeadsUpManagerImplOldTest.TEST_MINIMUM_DISPLAY_TIME;
+        mAutoDismissTime = HeadsUpManagerImplOldTest.TEST_AUTO_DISMISS_TIME;
+        mStickyForSomeTimeAutoDismissTime = HeadsUpManagerImplOldTest.TEST_STICKY_AUTO_DISMISS_TIME;
     }
 
     @NonNull
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorTest.kt
new file mode 100644
index 0000000..8acf538
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorTest.kt
@@ -0,0 +1,123 @@
+/*
+ * 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.domain.interactor
+
+import android.app.ActivityManager
+import android.media.AudioManager
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.plugins.fakeVolumeDialogController
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+class VolumeDialogSafetyWarningInteractorTest : SysuiTestCase() {
+
+    private val kosmos: Kosmos = testKosmos()
+
+    private lateinit var underTest: VolumeDialogSafetyWarningInteractor
+
+    @Before
+    fun setup() {
+        kosmos.useUnconfinedTestDispatcher()
+        underTest = kosmos.volumeDialogSafetyWarningInteractor
+    }
+
+    @Test
+    fun dismiss_isShowingSafetyWarning_isFalse() =
+        with(kosmos) {
+            runTest {
+                val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+
+                underTest.onSafetyWarningDismissed()
+
+                assertThat(isShowingSafetyWarning).isFalse()
+            }
+        }
+
+    @Test
+    fun flagShowUi_isShowingSafetyWarning_isTrue() =
+        with(kosmos) {
+            runTest {
+                val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+
+                fakeVolumeDialogController.onShowSafetyWarning(AudioManager.FLAG_SHOW_UI)
+
+                assertThat(isShowingSafetyWarning).isTrue()
+            }
+        }
+
+    @Test
+    fun flagShowUiWarnings_isShowingSafetyWarning_isTrue() =
+        with(kosmos) {
+            runTest {
+                val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+
+                fakeVolumeDialogController.onShowSafetyWarning(AudioManager.FLAG_SHOW_UI_WARNINGS)
+
+                assertThat(isShowingSafetyWarning).isTrue()
+            }
+        }
+
+    @Test
+    fun invisibleAndNoFlags_isShowingSafetyWarning_isFalse() =
+        with(kosmos) {
+            runTest {
+                val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+                volumeDialogVisibilityRepository.updateVisibility {
+                    VolumeDialogVisibilityModel.Invisible
+                }
+
+                fakeVolumeDialogController.onShowSafetyWarning(0)
+
+                assertThat(isShowingSafetyWarning).isFalse()
+            }
+        }
+
+    @Test
+    fun visibleAndNoFlags_isShowingSafetyWarning_isTrue() =
+        with(kosmos) {
+            runTest {
+                val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+                volumeDialogVisibilityRepository.updateVisibility {
+                    VolumeDialogVisibilityModel.Visible(
+                        Events.SHOW_REASON_VOLUME_CHANGED,
+                        false,
+                        ActivityManager.LOCK_TASK_MODE_LOCKED,
+                    )
+                }
+
+                fakeVolumeDialogController.onShowSafetyWarning(0)
+
+                assertThat(isShowingSafetyWarning).isTrue()
+            }
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt
index bba0050..a42ae03 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt
@@ -18,6 +18,7 @@
 
 import android.content.res.Resources
 import com.android.internal.R
+import com.android.systemui.common.ui.GlobalConfig
 import com.android.systemui.common.ui.data.repository.ConfigurationRepository
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -36,7 +37,7 @@
 constructor(
     @Application private val applicationScope: CoroutineScope,
     @Main private val resources: Resources,
-    configurationRepository: ConfigurationRepository,
+    @GlobalConfig configurationRepository: ConfigurationRepository,
 ) {
     /**
      * Whether to enable emergency services calls while the SIM card is locked. This is disabled in
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
index 61cd7c7..641400a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -42,6 +42,7 @@
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.res.R
 import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.shared.system.SysUiStatsLog
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
@@ -71,7 +72,7 @@
     private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
     private val falsingCollector: FalsingCollector,
     private val dismissCallbackRegistry: DismissCallbackRegistry,
-    private val context: Context,
+    @ShadeDisplayAware private val context: Context,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val trustRepository: TrustRepository,
     @Application private val applicationScope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
index 1aaf4fb..ec9ee91 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
@@ -37,6 +37,7 @@
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.res.R
+import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.util.icuMessageFormat
 import javax.inject.Inject
@@ -62,7 +63,7 @@
     @Background private val backgroundDispatcher: CoroutineDispatcher,
     private val repository: SimBouncerRepository,
     private val telephonyManager: TelephonyManager,
-    @Main private val resources: Resources,
+    @ShadeDisplayAware private val resources: Resources,
     private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
     private val euiccManager: EuiccManager?,
     // TODO(b/307977401): Replace this with `MobileConnectionsInteractor` when available.
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/docking/ui/viewmodel/KeyboardDockingIndicationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/docking/ui/viewmodel/KeyboardDockingIndicationViewModel.kt
index 38fc2a8..84a423e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/docking/ui/viewmodel/KeyboardDockingIndicationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/docking/ui/viewmodel/KeyboardDockingIndicationViewModel.kt
@@ -21,11 +21,12 @@
 import android.view.WindowManager
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.settingslib.Utils
+import com.android.systemui.common.ui.GlobalConfig
 import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyboard.docking.domain.interactor.KeyboardDockingIndicationInteractor
-import com.android.systemui.shade.ShadeDisplayAware
 import com.android.systemui.surfaceeffects.glowboxeffect.GlowBoxConfig
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -37,9 +38,9 @@
 @Inject
 constructor(
     private val windowManager: WindowManager,
-    private val context: Context,
+    @Application private val context: Context,
     keyboardDockingIndicationInteractor: KeyboardDockingIndicationInteractor,
-    @ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
+    @GlobalConfig configurationInteractor: ConfigurationInteractor,
     @Background private val backgroundScope: CoroutineScope,
 ) {
 
diff --git a/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt b/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt
index debb667..a19c9b3 100644
--- a/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.mediarouter.data.repository
 
+import android.media.projection.StopReason
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
@@ -40,7 +41,7 @@
     val castDevices: StateFlow<List<CastDevice>>
 
     /** Stops the cast to the given device. */
-    fun stopCasting(device: CastDevice)
+    fun stopCasting(device: CastDevice, @StopReason stopReason: Int)
 }
 
 @SysUISingleton
@@ -67,8 +68,8 @@
             .map { it.filter { device -> device.origin == CastDevice.CastOrigin.MediaRouter } }
             .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
 
-    override fun stopCasting(device: CastDevice) {
-        castController.stopCasting(device)
+    override fun stopCasting(device: CastDevice, @StopReason stopReason: Int) {
+        castController.stopCasting(device, stopReason)
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index eff5fc0..d8fc52b 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -216,20 +216,13 @@
             "handleKeyGestureEvent: Received OPEN_NOTES gesture event from keycodes: " +
                 event.keycodes.contentToString()
         }
-        if (
-            event.keycodes.contains(KEYCODE_N) &&
-                event.hasModifiers(KeyEvent.META_CTRL_ON or KeyEvent.META_META_ON)
-        ) {
-            debugLog { "Note task triggered by keyboard shortcut" }
-            backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) }
-            return true
-        }
         if (event.keycodes.size == 1 && event.keycodes[0] == KEYCODE_STYLUS_BUTTON_TAIL) {
             debugLog { "Note task triggered by stylus tail button" }
             backgroundExecutor.execute { controller.showNoteTask(TAIL_BUTTON) }
             return true
         }
-        return false
+        backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) }
+        return true
     }
 
     private fun isKeyGestureSupported(gestureType: Int): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index 8a72e8d..00b4524 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -24,6 +24,7 @@
 import android.app.Dialog;
 import android.content.Intent;
 import android.media.MediaRouter.RouteInfo;
+import android.media.projection.StopReason;
 import android.os.Handler;
 import android.os.Looper;
 import android.provider.Settings;
@@ -183,7 +184,7 @@
                 });
             }
         } else {
-            mController.stopCasting(activeDevices.get(0));
+            mController.stopCasting(activeDevices.get(0), StopReason.STOP_QS_TILE);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index f3be340..564d166 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -18,6 +18,7 @@
 
 import android.app.Dialog;
 import android.content.Intent;
+import android.media.projection.StopReason;
 import android.os.Handler;
 import android.os.Looper;
 import android.service.quicksettings.Tile;
@@ -225,7 +226,7 @@
     }
 
     private void stopRecording() {
-        mController.stopRecording();
+        mController.stopRecording(StopReason.STOP_QS_TILE);
     }
 
     private final class Callback implements RecordingController.RecordingStateChangeCallback {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
index 85aa674..9453447 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.qs.tiles.impl.screenrecord.domain.interactor
 
+import android.media.projection.StopReason
 import android.util.Log
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.systemui.animation.DialogCuj
@@ -61,7 +62,9 @@
                             Log.d(TAG, "Cancelling countdown")
                             withContext(backgroundContext) { recordingController.cancelCountdown() }
                         }
-                        is ScreenRecordModel.Recording -> screenRecordRepository.stopRecording()
+                        is ScreenRecordModel.Recording -> {
+                            screenRecordRepository.stopRecording(StopReason.STOP_QS_TILE)
+                        }
                         is ScreenRecordModel.DoingNothing ->
                             withContext(mainContext) {
                                 showPrompt(action.expandable, user.identifier)
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index d7463f8..9ee99e4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -23,6 +23,7 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.media.projection.StopReason;
 import android.os.Bundle;
 import android.os.CountDownTimer;
 import android.os.Process;
@@ -58,6 +59,7 @@
     private boolean mIsStarting;
     private boolean mIsRecording;
     private PendingIntent mStopIntent;
+    private @StopReason int mStopReason = StopReason.STOP_UNKNOWN;
     private final Bundle mInteractiveBroadcastOption;
     private CountDownTimer mCountDownTimer = null;
     private final Executor mMainExecutor;
@@ -83,7 +85,7 @@
             new UserTracker.Callback() {
                 @Override
                 public void onUserChanged(int newUser, @NonNull Context userContext) {
-                    stopRecording();
+                    stopRecording(StopReason.STOP_USER_SWITCH);
                 }
             };
 
@@ -240,9 +242,11 @@
     }
 
     /**
-     * Stop the recording
+     * Stop the recording and sets the stop reason to be used by the RecordingService
+     * @param stopReason the method of the recording stopped (i.e. QS tile, status bar chip, etc.)
      */
-    public void stopRecording() {
+    public void stopRecording(@StopReason int stopReason) {
+        mStopReason = stopReason;
         try {
             if (mStopIntent != null) {
                 mRecordingControllerLogger.logRecordingStopped();
@@ -277,6 +281,10 @@
         }
     }
 
+    public @StopReason int getStopReason() {
+        return mStopReason;
+    }
+
     @Override
     public void addCallback(@NonNull RecordingStateChangeCallback listener) {
         mListeners.add(listener);
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 8c207d1..f7b5271 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -26,6 +26,7 @@
 import android.content.Intent;
 import android.graphics.drawable.Icon;
 import android.media.MediaRecorder;
+import android.media.projection.StopReason;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -78,6 +79,7 @@
     private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
     private static final String EXTRA_CAPTURE_TARGET = "extra_captureTarget";
     private static final String EXTRA_DISPLAY_ID = "extra_displayId";
+    private static final String EXTRA_STOP_REASON = "extra_stopReason";
 
     protected static final String ACTION_START = "com.android.systemui.screenrecord.START";
     protected static final String ACTION_SHOW_START_NOTIF =
@@ -242,7 +244,8 @@
                 // Check user ID - we may be getting a stop intent after user switch, in which case
                 // we want to post the notifications for that user, which is NOT current user
                 int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_ID_NOT_SPECIFIED);
-                stopService(userId);
+                int stopReason = intent.getIntExtra(EXTRA_STOP_REASON, mController.getStopReason());
+                stopService(userId, stopReason);
                 break;
 
             case ACTION_SHARE:
@@ -486,11 +489,11 @@
                 getTag(), notificationIdForGroup, groupNotif, currentUser);
     }
 
-    private void stopService() {
-        stopService(USER_ID_NOT_SPECIFIED);
+    private void stopService(@StopReason int stopReason) {
+        stopService(USER_ID_NOT_SPECIFIED, stopReason);
     }
 
-    private void stopService(int userId) {
+    private void stopService(int userId, @StopReason int stopReason) {
         if (userId == USER_ID_NOT_SPECIFIED) {
             userId = mUserContextTracker.getUserContext().getUserId();
         }
@@ -499,7 +502,7 @@
         setTapsVisible(mOriginalShowTaps);
         try {
             if (getRecorder() != null) {
-                getRecorder().end();
+                getRecorder().end(stopReason);
             }
             saveRecording(userId);
         } catch (RuntimeException exception) {
@@ -598,7 +601,8 @@
      * @return
      */
     protected Intent getNotificationIntent(Context context) {
-        return new Intent(context, this.getClass()).setAction(ACTION_STOP_NOTIF);
+        return new Intent(context, this.getClass()).setAction(ACTION_STOP_NOTIF)
+                .putExtra(EXTRA_STOP_REASON, StopReason.STOP_HOST_APP);
     }
 
     private Intent getShareIntent(Context context, Uri path) {
@@ -610,14 +614,17 @@
     @Override
     public void onInfo(MediaRecorder mr, int what, int extra) {
         Log.d(getTag(), "Media recorder info: " + what);
-        onStartCommand(getStopIntent(this), 0, 0);
+        // Stop due to record reaching size limits so log as stopping due to error
+        Intent stopIntent = getStopIntent(this);
+        stopIntent.putExtra(EXTRA_STOP_REASON, StopReason.STOP_ERROR);
+        onStartCommand(stopIntent, 0, 0);
     }
 
     @Override
-    public void onStopped() {
+    public void onStopped(@StopReason int stopReason) {
         if (mController.isRecording()) {
             Log.d(getTag(), "Stopping recording because the system requested the stop");
-            stopService();
+            stopService(stopReason);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index 2ca0621..f4455bf 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -41,6 +41,7 @@
 import android.media.projection.IMediaProjectionManager;
 import android.media.projection.MediaProjection;
 import android.media.projection.MediaProjectionManager;
+import android.media.projection.StopReason;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.IBinder;
@@ -300,7 +301,7 @@
     /**
      * End screen recording, throws an exception if stopping recording failed
      */
-    void end() throws IOException {
+    void end(@StopReason int stopReason) throws IOException {
         Closer closer = new Closer();
 
         // MediaRecorder might throw RuntimeException if stopped immediately after starting
@@ -309,7 +310,17 @@
         closer.register(mMediaRecorder::release);
         closer.register(mInputSurface::release);
         closer.register(mVirtualDisplay::release);
-        closer.register(mMediaProjection::stop);
+        closer.register(() -> {
+            if (stopReason == StopReason.STOP_UNKNOWN) {
+                // Attempt to call MediaProjection#stop() even if it might have already been called.
+                // If projection has already been stopped, then nothing will happen. Else, stop
+                // will be logged as a manually requested stop from host app.
+                mMediaProjection.stop();
+            } else {
+                // In any other case, the stop reason is related to the recorder, so pass it on here
+                mMediaProjection.stop(stopReason);
+            }
+        });
         closer.register(this::stopInternalAudioRecording);
 
         closer.close();
@@ -323,7 +334,7 @@
     @Override
     public void onStop() {
         Log.d(TAG, "The system notified about stopping the projection");
-        mListener.onStopped();
+        mListener.onStopped(StopReason.STOP_UNKNOWN);
     }
 
     private void stopInternalAudioRecording() {
@@ -453,7 +464,7 @@
          * For example, this might happen when doing partial screen sharing of an app
          * and the app that is being captured is closed.
          */
-        void onStopped();
+        void onStopped(@StopReason int stopReason);
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
index 9eeb3b9..b6b8ffa 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.screenrecord.data.repository
 
+import android.media.projection.StopReason
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.screenrecord.RecordingController
@@ -41,7 +42,7 @@
     val screenRecordState: Flow<ScreenRecordModel>
 
     /** Stops the recording. */
-    suspend fun stopRecording()
+    suspend fun stopRecording(@StopReason stopReason: Int)
 }
 
 @SysUISingleton
@@ -95,7 +96,7 @@
         }
     }
 
-    override suspend fun stopRecording() {
-        withContext(bgCoroutineContext) { recordingController.stopRecording() }
+    override suspend fun stopRecording(@StopReason stopReason: Int) {
+        withContext(bgCoroutineContext) { recordingController.stopRecording(stopReason) }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
index b3dbf29..229cef9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor
 
+import android.media.projection.StopReason
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.log.LogBuffer
@@ -65,7 +66,9 @@
 
     /** Stops the currently active MediaRouter cast. */
     fun stopCasting() {
-        activeCastDevice.value?.let { mediaRouterRepository.stopCasting(it) }
+        activeCastDevice.value?.let {
+            mediaRouterRepository.stopCasting(it, StopReason.STOP_PRIVACY_CHIP)
+        }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index f5952f4..0b5e669 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
 
+import android.media.projection.StopReason
 import com.android.systemui.Flags
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -140,7 +141,7 @@
 
     /** Stops the recording. */
     fun stopRecording() {
-        scope.launch { screenRecordRepository.stopRecording() }
+        scope.launch { screenRecordRepository.stopRecording(StopReason.STOP_PRIVACY_CHIP) }
     }
 
     companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java
index a3dcc3b..ece5a3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy;
 
+import android.media.projection.StopReason;
 import com.android.systemui.Dumpable;
 import com.android.systemui.statusbar.policy.CastController.Callback;
 
@@ -26,7 +27,7 @@
     void setCurrentUserId(int currentUserId);
     List<CastDevice> getCastDevices();
     void startCasting(CastDevice device);
-    void stopCasting(CastDevice device);
+    void stopCasting(CastDevice device, @StopReason int stopReason);
 
     /**
      * @return whether we have a connected device.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
index 52f80fb..ab20850 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
@@ -185,13 +185,13 @@
     }
 
     @Override
-    public void stopCasting(CastDevice device) {
+    public void stopCasting(CastDevice device, @StopReason int stopReason) {
         final boolean isProjection = device.getTag() instanceof MediaProjectionInfo;
         mLogger.logStopCasting(isProjection);
         if (isProjection) {
             final MediaProjectionInfo projection = (MediaProjectionInfo) device.getTag();
             if (Objects.equals(mProjectionManager.getActiveProjectionInfo(), projection)) {
-                mProjectionManager.stopActiveProjection(StopReason.STOP_QS_TILE);
+                mProjectionManager.stopActiveProjection(stopReason);
             } else {
                 mLogger.logStopCastingNoProjection(projection);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
index 094ec39..203a157 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
@@ -16,19 +16,31 @@
 
 package com.android.systemui.volume.dialog
 
+import android.content.Context
+import android.media.AudioManager
 import com.android.app.tracing.coroutines.coroutineScopeTraced
 import com.android.app.tracing.coroutines.launchTraced as launch
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.plugins.VolumeDialog
+import com.android.systemui.volume.SafetyWarningDialog
 import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogPluginViewModel
 import javax.inject.Inject
+import kotlin.coroutines.resume
 import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.suspendCancellableCoroutine
 
+@OptIn(ExperimentalCoroutinesApi::class)
 class VolumeDialogPlugin
 @Inject
 constructor(
     @Application private val applicationCoroutineScope: CoroutineScope,
+    private val context: Context,
+    private val audioManager: AudioManager,
     private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
 ) : VolumeDialog {
 
@@ -41,14 +53,39 @@
                 coroutineScopeTraced("[Volume]plugin") {
                     pluginComponent =
                         volumeDialogPluginComponentFactory.create(this).also {
-                            it.viewModel().launchVolumeDialog()
+                            bindPlugin(it.viewModel())
                         }
                 }
             }
     }
 
+    private fun CoroutineScope.bindPlugin(viewModel: VolumeDialogPluginViewModel) {
+        viewModel.launchVolumeDialog()
+
+        viewModel.isShowingSafetyWarning
+            .mapLatest { isShowingSafetyWarning ->
+                if (isShowingSafetyWarning) {
+                    showSafetyWarningVisibility { viewModel.onSafetyWarningDismissed() }
+                }
+            }
+            .launchIn(this)
+    }
+
     override fun destroy() {
         job?.cancel()
         pluginComponent = null
     }
+
+    private suspend fun showSafetyWarningVisibility(onDismissed: () -> Unit) =
+        suspendCancellableCoroutine { continuation ->
+            val dialog =
+                object : SafetyWarningDialog(context, audioManager) {
+                    override fun cleanUp() {
+                        onDismissed()
+                        continuation.resume(Unit)
+                    }
+                }
+            dialog.show()
+            continuation.invokeOnCancellation { dialog.dismiss() }
+        }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractor.kt
new file mode 100644
index 0000000..f707d67
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractor.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.domain.interactor
+
+import android.media.AudioManager
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogSafetyWarningModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+
+private const val VISIBLE_FLAGS = AudioManager.FLAG_SHOW_UI or AudioManager.FLAG_SHOW_UI_WARNINGS
+
+@VolumeDialogPluginScope
+class VolumeDialogSafetyWarningInteractor
+@Inject
+constructor(
+    private val stateInteractor: VolumeDialogStateInteractor,
+    visibilityInteractor: VolumeDialogVisibilityInteractor,
+) {
+
+    val isShowingSafetyWarning: Flow<Boolean> =
+        stateInteractor.volumeDialogState.map {
+            when (it.isShowingSafetyWarning) {
+                is VolumeDialogSafetyWarningModel.Visible ->
+                    if (it.isShowingSafetyWarning.flags and VISIBLE_FLAGS == 0) {
+                        visibilityInteractor.dialogVisibility.first() is
+                            VolumeDialogVisibilityModel.Visible
+                    } else {
+                        true
+                    }
+                is VolumeDialogSafetyWarningModel.Invisible -> false
+            }
+        }
+
+    fun onSafetyWarningDismissed() {
+        stateInteractor.setSafetyWarning(VolumeDialogSafetyWarningModel.Invisible)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
index 5c7289b..51e7924 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
@@ -23,6 +23,7 @@
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
 import com.android.systemui.volume.dialog.data.repository.VolumeDialogStateRepository
 import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogSafetyWarningModel
 import com.android.systemui.volume.dialog.shared.model.VolumeDialogStateModel
 import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
 import javax.inject.Inject
@@ -61,6 +62,9 @@
                             oldState.copy(shouldShowA11ySlider = event.showA11yStream)
                         }
                     }
+                    is VolumeDialogEventModel.ShowSafetyWarning -> {
+                        setSafetyWarning(VolumeDialogSafetyWarningModel.Visible(event.flags))
+                    }
                     else -> {
                         // do nothing
                     }
@@ -72,6 +76,10 @@
 
     val volumeDialogState: Flow<VolumeDialogStateModel> = volumeDialogStateRepository.state
 
+    fun setSafetyWarning(model: VolumeDialogSafetyWarningModel) {
+        volumeDialogStateRepository.updateState { it.copy(isShowingSafetyWarning = model) }
+    }
+
     /** Returns a copy of [model] filled with the values from [VolumeDialogController.State]. */
     private fun VolumeDialogController.State.copyIntoModel(
         model: VolumeDialogStateModel
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogSafetyWarningModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogSafetyWarningModel.kt
new file mode 100644
index 0000000..39fc222
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogSafetyWarningModel.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.model
+
+/** Models current Volume Safety Dialog state. */
+sealed interface VolumeDialogSafetyWarningModel {
+
+    /** Volume Safety Dialog is visible and has been shown with the [flags]. */
+    data class Visible(val flags: Int) : VolumeDialogSafetyWarningModel
+
+    /** Volume Safety Dialog is invisible. */
+    data object Invisible : VolumeDialogSafetyWarningModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogStateModel.kt
index 1792b99..838006d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogStateModel.kt
@@ -21,6 +21,8 @@
 /** Models a state of the Volume Dialog. */
 data class VolumeDialogStateModel(
     val shouldShowA11ySlider: Boolean = false,
+    val isShowingSafetyWarning: VolumeDialogSafetyWarningModel =
+        VolumeDialogSafetyWarningModel.Invisible,
     val streamModels: Map<Int, VolumeDialogStreamModel> = mapOf(),
     val ringerModeInternal: Int = 0,
     val ringerModeExternal: Int = 0,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
index ff525f46..9bab1b0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
@@ -18,14 +18,17 @@
 
 import com.android.systemui.volume.Events
 import com.android.systemui.volume.dialog.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
 import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogSafetyWarningInteractor
 import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
 import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
 import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
 import javax.inject.Inject
 import javax.inject.Provider
+import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.mapLatest
 
@@ -34,29 +37,35 @@
 class VolumeDialogPluginViewModel
 @Inject
 constructor(
+    @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
     private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
+    private val dialogSafetyWarningInteractor: VolumeDialogSafetyWarningInteractor,
     private val volumeDialogProvider: Provider<VolumeDialog>,
     private val logger: VolumeDialogLogger,
 ) {
 
-    suspend fun launchVolumeDialog() {
-        coroutineScope {
-            dialogVisibilityInteractor.dialogVisibility
-                .mapLatest { visibilityModel ->
-                    with(visibilityModel) {
-                        if (this is VolumeDialogVisibilityModel.Visible) {
-                            showDialog()
-                            Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked)
-                            logger.onShow(reason)
-                        }
-                        if (this is VolumeDialogVisibilityModel.Dismissed) {
-                            Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason)
-                            logger.onDismiss(reason)
-                        }
+    fun launchVolumeDialog() {
+        dialogVisibilityInteractor.dialogVisibility
+            .mapLatest { visibilityModel ->
+                with(visibilityModel) {
+                    if (this is VolumeDialogVisibilityModel.Visible) {
+                        showDialog()
+                        Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked)
+                        logger.onShow(reason)
+                    }
+                    if (this is VolumeDialogVisibilityModel.Dismissed) {
+                        Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason)
+                        logger.onDismiss(reason)
                     }
                 }
-                .launchIn(this)
-        }
+            }
+            .launchIn(coroutineScope)
+    }
+
+    val isShowingSafetyWarning: Flow<Boolean> = dialogSafetyWarningInteractor.isShowingSafetyWarning
+
+    fun onSafetyWarningDismissed() {
+        dialogSafetyWarningInteractor.onSafetyWarningDismissed()
     }
 
     private fun showDialog() {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
index 7a6ede4..b20dffb 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
@@ -44,9 +44,9 @@
 @Inject
 constructor(
     private val context: Context,
-    dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
+    private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
     volumeDialogSlidersInteractor: VolumeDialogSlidersInteractor,
-    volumeDialogStateInteractor: VolumeDialogStateInteractor,
+    private val volumeDialogStateInteractor: VolumeDialogStateInteractor,
     devicePostureController: DevicePostureController,
     configurationController: ConfigurationController,
 ) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index d88b758..266cb51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -181,11 +181,9 @@
 
     @Test
     @EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
-    fun handlesShortcut_metaCtrlN() {
+    fun handlesShortcut_keyGestureTypeOpenNotes() {
         val gestureEvent =
             KeyGestureEvent.Builder()
-                .setKeycodes(intArrayOf(KeyEvent.KEYCODE_N))
-                .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON)
                 .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES)
                 .setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
                 .build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index afff485..a17f100 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -36,6 +36,7 @@
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
+import android.media.projection.StopReason;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 
@@ -154,7 +155,7 @@
         PendingIntent stopIntent = Mockito.mock(PendingIntent.class);
 
         mController.startCountdown(0, 0, startIntent, stopIntent);
-        mController.stopRecording();
+        mController.stopRecording(StopReason.STOP_UNKNOWN);
 
         assertFalse(mController.isStarting());
         assertFalse(mController.isRecording());
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt
index 8aa7a03..d5637cb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.mediarouter.data.repository
 
+import android.media.projection.StopReason
 import com.android.systemui.statusbar.policy.CastDevice
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -25,7 +26,7 @@
     var lastStoppedDevice: CastDevice? = null
         private set
 
-    override fun stopCasting(device: CastDevice) {
+    override fun stopCasting(device: CastDevice, @StopReason stopReason: Int) {
         lastStoppedDevice = device
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
index 30b4763..4c9e174 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.screenrecord.data.repository
 
+import android.media.projection.StopReason
 import com.android.systemui.screenrecord.data.model.ScreenRecordModel
 import kotlinx.coroutines.flow.MutableStateFlow
 
@@ -25,7 +26,7 @@
 
     var stopRecordingInvoked = false
 
-    override suspend fun stopRecording() {
+    override suspend fun stopRecording(@StopReason stopReason: Int) {
         stopRecordingInvoked = true
     }
 }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt
index 2df0c7a5..da6b2ae 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy
 
+import android.media.projection.StopReason
 import java.io.PrintWriter
 
 class FakeCastController : CastController {
@@ -45,7 +46,7 @@
 
     override fun startCasting(device: CastDevice?) {}
 
-    override fun stopCasting(device: CastDevice?) {
+    override fun stopCasting(device: CastDevice?, @StopReason stopReason: Int) {
         lastStoppedDevice = device
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java
index 2249bc0..857dc85 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.utils.leaks;
 
+import android.media.projection.StopReason;
 import android.testing.LeakCheck;
 
 import com.android.systemui.statusbar.policy.CastController;
@@ -51,7 +52,7 @@
     }
 
     @Override
-    public void stopCasting(CastDevice device) {
+    public void stopCasting(CastDevice device, @StopReason int stopReason) {
 
     }
 
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorKosmos.kt
new file mode 100644
index 0000000..ddd0014
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.volumeDialogSafetyWarningInteractor: VolumeDialogSafetyWarningInteractor by
+    Kosmos.Fixture {
+        VolumeDialogSafetyWarningInteractor(
+            volumeDialogStateInteractor,
+            volumeDialogVisibilityInteractor,
+        )
+    }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index c4e1036..e10bdaa 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -765,6 +765,7 @@
     private float mBacklightMinimum = Float.NaN;
     private float mBacklightMaximum = Float.NaN;
     private float mBrightnessDefault = Float.NaN;
+    private float mBrightnessDim = Float.NaN;
     private float mBrightnessRampFastDecrease = Float.NaN;
     private float mBrightnessRampFastIncrease = Float.NaN;
     private float mBrightnessRampSlowDecrease = Float.NaN;
@@ -1282,6 +1283,24 @@
     }
 
     /**
+     * Return the minimum brightness on a scale of 0.0f - 1.0f
+     *
+     * @return minimum brightness
+     */
+    public float getBrightnessMinimum() {
+        return getBrightnessFromBacklight(mBacklightMinimum);
+    }
+
+    /**
+     * Return the maximum brightness on a scale of 0.0f - 1.0f
+     *
+     * @return maximum brightness
+     */
+    public float getBrightnessMaximum() {
+        return getBrightnessFromBacklight(mBacklightMaximum);
+    }
+
+    /**
      * Return the default brightness on a scale of 0.0f - 1.0f
      *
      * @return default brightness
@@ -1290,6 +1309,15 @@
         return mBrightnessDefault;
     }
 
+    /**
+     * Return the dim brightness on a scale of 0.0f - 1.0f
+     *
+     * @return dim brightness
+     */
+    public float getBrightnessDim() {
+        return mBrightnessDim;
+    }
+
     public float getBrightnessRampFastDecrease() {
         return mBrightnessRampFastDecrease;
     }
@@ -1689,6 +1717,7 @@
                 + ", mBacklightMinimum=" + mBacklightMinimum
                 + ", mBacklightMaximum=" + mBacklightMaximum
                 + ", mBrightnessDefault=" + mBrightnessDefault
+                + ", mBrightnessDim=" + mBrightnessDim
                 + ", mQuirks=" + mQuirks
                 + "\n"
                 + "mLuxThrottlingData=" + mLuxThrottlingData
@@ -1906,6 +1935,7 @@
         mBacklightMinimum = PowerManager.BRIGHTNESS_MIN;
         mBacklightMaximum = PowerManager.BRIGHTNESS_MAX;
         mBrightnessDefault = BRIGHTNESS_DEFAULT;
+        mBrightnessDim = PowerManager.BRIGHTNESS_INVALID;
         mBrightnessRampFastDecrease = PowerManager.BRIGHTNESS_MAX;
         mBrightnessRampFastIncrease = PowerManager.BRIGHTNESS_MAX;
         mBrightnessRampSlowDecrease = PowerManager.BRIGHTNESS_MAX;
@@ -2003,6 +2033,15 @@
             mBacklightMinimum = min;
             mBacklightMaximum = max;
         }
+        final float dim = mContext.getResources().getFloat(com.android.internal.R.dimen
+                .config_screenBrightnessDimFloat);
+        if (dim == INVALID_BRIGHTNESS_IN_CONFIG) {
+            mBrightnessDim = BrightnessSynchronizer.brightnessIntToFloat(
+                    mContext.getResources().getInteger(com.android.internal.R.integer
+                            .config_screenBrightnessDim));
+        } else {
+            mBrightnessDim = dim;
+        }
     }
 
     private void loadBrightnessMap(DisplayConfiguration config) {
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index 1c1bdad..3aaf4f6 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -477,6 +477,7 @@
     public float brightnessMinimum;
     public float brightnessMaximum;
     public float brightnessDefault;
+    public float brightnessDim;
 
     // NaN means unsupported
     public float hdrSdrRatio = Float.NaN;
@@ -561,8 +562,8 @@
                 || !Objects.equals(ownerPackageName, other.ownerPackageName)
                 || !BrightnessSynchronizer.floatEquals(brightnessMinimum, other.brightnessMinimum)
                 || !BrightnessSynchronizer.floatEquals(brightnessMaximum, other.brightnessMaximum)
-                || !BrightnessSynchronizer.floatEquals(brightnessDefault,
-                other.brightnessDefault)
+                || !BrightnessSynchronizer.floatEquals(brightnessDefault, other.brightnessDefault)
+                || !BrightnessSynchronizer.floatEquals(brightnessDim, other.brightnessDim)
                 || !Objects.equals(roundedCorners, other.roundedCorners)
                 || installOrientation != other.installOrientation
                 || !Objects.equals(displayShape, other.displayShape)
@@ -618,6 +619,7 @@
         brightnessMinimum = other.brightnessMinimum;
         brightnessMaximum = other.brightnessMaximum;
         brightnessDefault = other.brightnessDefault;
+        brightnessDim = other.brightnessDim;
         hdrSdrRatio = other.hdrSdrRatio;
         roundedCorners = other.roundedCorners;
         installOrientation = other.installOrientation;
@@ -672,6 +674,7 @@
         sb.append(", brightnessMinimum ").append(brightnessMinimum);
         sb.append(", brightnessMaximum ").append(brightnessMaximum);
         sb.append(", brightnessDefault ").append(brightnessDefault);
+        sb.append(", brightnessDim ").append(brightnessDim);
         sb.append(", hdrSdrRatio ").append(hdrSdrRatio);
         if (roundedCorners != null) {
             sb.append(", roundedCorners ").append(roundedCorners);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 0b8f7d5..d37dd30 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -812,9 +812,10 @@
 
                 // The display is trusted since it is created by system.
                 mInfo.flags |= DisplayDeviceInfo.FLAG_TRUSTED;
-                mInfo.brightnessMinimum = PowerManager.BRIGHTNESS_MIN;
-                mInfo.brightnessMaximum = PowerManager.BRIGHTNESS_MAX;
+                mInfo.brightnessMinimum = getDisplayDeviceConfig().getBrightnessMinimum();
+                mInfo.brightnessMaximum = getDisplayDeviceConfig().getBrightnessMaximum();
                 mInfo.brightnessDefault = getDisplayDeviceConfig().getBrightnessDefault();
+                mInfo.brightnessDim = getDisplayDeviceConfig().getBrightnessDim();
                 mInfo.hdrSdrRatio = mCurrentHdrSdrRatio;
             }
             return mInfo;
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 8546598..1de9c95 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -548,6 +548,7 @@
             mBaseDisplayInfo.brightnessMinimum = deviceInfo.brightnessMinimum;
             mBaseDisplayInfo.brightnessMaximum = deviceInfo.brightnessMaximum;
             mBaseDisplayInfo.brightnessDefault = deviceInfo.brightnessDefault;
+            mBaseDisplayInfo.brightnessDim = deviceInfo.brightnessDim;
             mBaseDisplayInfo.hdrSdrRatio = deviceInfo.hdrSdrRatio;
             mBaseDisplayInfo.roundedCorners = deviceInfo.roundedCorners;
             mBaseDisplayInfo.installOrientation = deviceInfo.installOrientation;
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index 6ae58c4..836f4ed 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -340,6 +340,7 @@
         private boolean mIsWindowManagerMirroring;
         private final DisplayCutout mDisplayCutout;
         private final float mDefaultBrightness;
+        private final float mDimBrightness;
         private float mCurrentBrightness;
         private final IBrightnessListener mBrightnessListener;
 
@@ -359,6 +360,7 @@
             mRequestedRefreshRate = virtualDisplayConfig.getRequestedRefreshRate();
             mDisplayCutout = virtualDisplayConfig.getDisplayCutout();
             mDefaultBrightness = virtualDisplayConfig.getDefaultBrightness();
+            mDimBrightness = virtualDisplayConfig.getDimBrightness();
             mCurrentBrightness = PowerManager.BRIGHTNESS_INVALID;
             mBrightnessListener = virtualDisplayConfig.getBrightnessListener();
             mMode = createMode(mWidth, mHeight, getRefreshRate());
@@ -645,6 +647,7 @@
                 mInfo.brightnessMinimum = PowerManager.BRIGHTNESS_MIN;
                 mInfo.brightnessMaximum = PowerManager.BRIGHTNESS_MAX;
                 mInfo.brightnessDefault = mDefaultBrightness;
+                mInfo.brightnessDim = mDimBrightness;
 
                 mInfo.ownerUid = mOwnerUid;
                 mInfo.ownerPackageName = mOwnerPackageName;
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 7ccab05..ceade37 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -267,7 +267,7 @@
                 }
             }
 
-            modifiers.add(new DisplayDimModifier(context));
+            modifiers.add(new DisplayDimModifier(data.mDisplayId, context));
             modifiers.add(new BrightnessLowPowerModeModifier());
             if (flags.isEvenDimmerEnabled() && data.mDisplayDeviceConfig.isEvenDimmerAvailable()) {
                 modifiers.add(new BrightnessLowLuxModifier(handler, listener, context,
diff --git a/services/core/java/com/android/server/display/brightness/clamper/DisplayDimModifier.java b/services/core/java/com/android/server/display/brightness/clamper/DisplayDimModifier.java
index ab880bf..0237af3 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/DisplayDimModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/DisplayDimModifier.java
@@ -38,12 +38,12 @@
     // mScreenBrightnessDimConfig.
     private final float mScreenBrightnessMinimumDimAmount;
 
-    DisplayDimModifier(Context context) {
+    DisplayDimModifier(int displayId, Context context) {
         PowerManager pm = Objects.requireNonNull(context.getSystemService(PowerManager.class));
         Resources resources = context.getResources();
 
         mScreenBrightnessDimConfig = BrightnessUtils.clampAbsoluteBrightness(
-                pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM));
+                pm.getBrightnessConstraint(displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM));
         mScreenBrightnessMinimumDimAmount = resources.getFloat(
                 R.dimen.config_screenBrightnessMinimumDimAmountFloat);
     }
diff --git a/services/core/java/com/android/server/pm/InstallDependencyHelper.java b/services/core/java/com/android/server/pm/InstallDependencyHelper.java
index 13aab11..673a102 100644
--- a/services/core/java/com/android/server/pm/InstallDependencyHelper.java
+++ b/services/core/java/com/android/server/pm/InstallDependencyHelper.java
@@ -32,11 +32,13 @@
 import android.content.pm.dependencyinstaller.IDependencyInstallerCallback;
 import android.content.pm.dependencyinstaller.IDependencyInstallerService;
 import android.content.pm.parsing.PackageLite;
+import android.os.Binder;
 import android.os.Handler;
 import android.os.OutcomeReceiver;
 import android.os.Process;
 import android.os.RemoteException;
 import android.os.UserHandle;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.Slog;
 
@@ -63,12 +65,11 @@
     private final Context mContext;
     private final SharedLibrariesImpl mSharedLibraries;
     private final PackageInstallerService mPackageInstallerService;
-    private final Object mRemoteServiceLock = new Object();
     @GuardedBy("mTrackers")
     private final List<DependencyInstallTracker> mTrackers = new ArrayList<>();
-
-    @GuardedBy("mRemoteServiceLock")
-    private ServiceConnector<IDependencyInstallerService> mRemoteService = null;
+    @GuardedBy("mRemoteServices")
+    private final ArrayMap<Integer, ServiceConnector<IDependencyInstallerService>> mRemoteServices =
+            new ArrayMap<>();
 
     InstallDependencyHelper(Context context, SharedLibrariesImpl sharedLibraries,
             PackageInstallerService packageInstallerService) {
@@ -97,13 +98,18 @@
 
         if (missing.isEmpty()) {
             if (DEBUG) {
-                Slog.d(TAG, "No missing dependency for " + pkg);
+                Slog.d(TAG, "No missing dependency for " + pkg.getPackageName());
             }
             // No need for dependency resolution. Move to installation directly.
             callback.onResult(null);
             return;
         }
 
+        if (DEBUG) {
+            Slog.d(TAG, "Missing dependencies found for pkg: " + pkg.getPackageName()
+                    + " user: " + userId);
+        }
+
         if (!bindToDependencyInstallerIfNeeded(userId, handler, snapshot)) {
             onError(callback, "Dependency Installer Service not found");
             return;
@@ -112,8 +118,8 @@
         IDependencyInstallerCallback serviceCallback =
                 new DependencyInstallerCallbackCallOnce(handler, callback, userId);
         boolean scheduleSuccess;
-        synchronized (mRemoteServiceLock) {
-            scheduleSuccess = mRemoteService.run(service -> {
+        synchronized (mRemoteServices) {
+            scheduleSuccess = mRemoteServices.get(userId).run(service -> {
                 service.onDependenciesRequired(missing,
                         new DependencyInstallerCallback(serviceCallback.asBinder()));
             });
@@ -149,15 +155,17 @@
 
     private boolean bindToDependencyInstallerIfNeeded(int userId, Handler handler,
             Computer snapshot) {
-        synchronized (mRemoteServiceLock) {
-            if (mRemoteService != null) {
+        synchronized (mRemoteServices) {
+            if (mRemoteServices.containsKey(userId)) {
                 if (DEBUG) {
-                    Slog.i(TAG, "DependencyInstallerService already bound");
+                    Slog.i(TAG, "DependencyInstallerService for user " + userId + " already bound");
                 }
                 return true;
             }
         }
 
+        Slog.i(TAG, "Attempting to bind to Dependency Installer Service for user " + userId);
+
         RoleManager roleManager = mContext.getSystemService(RoleManager.class);
         if (roleManager == null) {
             Slog.w(TAG, "Cannot find RoleManager system service");
@@ -166,7 +174,7 @@
         List<String> holders = roleManager.getRoleHoldersAsUser(
                 ROLE_SYSTEM_DEPENDENCY_INSTALLER, UserHandle.of(userId));
         if (holders.isEmpty()) {
-            Slog.w(TAG, "No holders of ROLE_SYSTEM_DEPENDENCY_INSTALLER found");
+            Slog.w(TAG, "No holders of ROLE_SYSTEM_DEPENDENCY_INSTALLER found for user " + userId);
             return false;
         }
 
@@ -178,6 +186,8 @@
                 /*includeInstantApps*/ false, /*resolveForStart*/ false);
 
         if (resolvedIntents.isEmpty()) {
+            Slog.w(TAG, "No package holding ROLE_SYSTEM_DEPENDENCY_INSTALLER found for user "
+                    + userId);
             return false;
         }
 
@@ -206,13 +216,14 @@
                 };
 
 
-        synchronized (mRemoteServiceLock) {
+        synchronized (mRemoteServices) {
             // Some other thread managed to connect to the service first
-            if (mRemoteService != null) {
+            if (mRemoteServices.containsKey(userId)) {
                 return true;
             }
-            mRemoteService = serviceConnector;
-            mRemoteService.setServiceLifecycleCallbacks(
+            mRemoteServices.put(userId, serviceConnector);
+            // Block the lock until we connect to the service
+            serviceConnector.setServiceLifecycleCallbacks(
                 new ServiceConnector.ServiceLifecycleCallbacks<>() {
                     @Override
                     public void onDisconnected(@NonNull IDependencyInstallerService service) {
@@ -228,17 +239,18 @@
                     }
 
                     private void destroy() {
-                        synchronized (mRemoteServiceLock) {
-                            if (mRemoteService != null) {
-                                mRemoteService.unbind();
-                                mRemoteService = null;
+                        synchronized (mRemoteServices) {
+                            if (mRemoteServices.containsKey(userId)) {
+                                mRemoteServices.get(userId).unbind();
+                                mRemoteServices.remove(userId);
                             }
                         }
                     }
 
                 });
-            AndroidFuture<IDependencyInstallerService> unusedFuture = mRemoteService.connect();
+            AndroidFuture<IDependencyInstallerService> unusedFuture = serviceConnector.connect();
         }
+        Slog.i(TAG, "Successfully bound to Dependency Installer Service for user " + userId);
         return true;
     }
 
@@ -318,30 +330,40 @@
                 Slog.d(TAG, "onAllDependenciesResolved started");
             }
 
-            // Before creating any tracker, validate the arguments
-            ArraySet<Integer> validSessionIds = validateSessionIds(sessionIds);
+            try {
+                // Before creating any tracker, validate the arguments
+                ArraySet<Integer> validSessionIds = validateSessionIds(sessionIds);
 
-            if (validSessionIds.isEmpty()) {
-                mCallback.onResult(null);
-                return;
-            }
-
-            // Create a tracker now if there are any pending sessions remaining.
-            DependencyInstallTracker tracker = new DependencyInstallTracker(
-                    mCallback, validSessionIds);
-            synchronized (mTrackers) {
-                mTrackers.add(tracker);
-            }
-
-            // By the time the tracker was created, some of the sessions in validSessionIds
-            // could have finished. Avoid waiting for them indefinitely.
-            for (int sessionId : validSessionIds) {
-                SessionInfo sessionInfo = mPackageInstallerService.getSessionInfo(sessionId);
-
-                // Don't wait for sessions that finished already
-                if (sessionInfo == null) {
-                    notifySessionComplete(sessionId, /*success=*/ true);
+                if (validSessionIds.isEmpty()) {
+                    mCallback.onResult(null);
+                    return;
                 }
+
+                // Create a tracker now if there are any pending sessions remaining.
+                DependencyInstallTracker tracker = new DependencyInstallTracker(
+                        mCallback, validSessionIds);
+                synchronized (mTrackers) {
+                    mTrackers.add(tracker);
+                }
+
+                // By the time the tracker was created, some of the sessions in validSessionIds
+                // could have finished. Avoid waiting for them indefinitely.
+                for (int sessionId : validSessionIds) {
+                    SessionInfo sessionInfo = mPackageInstallerService.getSessionInfo(sessionId);
+
+                    // Don't wait for sessions that finished already
+                    if (sessionInfo == null) {
+                        Binder.withCleanCallingIdentity(() -> {
+                            notifySessionComplete(sessionId, /*success=*/ true);
+                        });
+                    }
+                }
+            } catch (Exception e) {
+                // Allow calling the callback again
+                synchronized (this) {
+                    mDependencyInstallerCallbackInvoked = false;
+                }
+                throw e;
             }
         }
 
@@ -354,7 +376,10 @@
                 }
                 mDependencyInstallerCallbackInvoked = true;
             }
-            onError(mCallback, "Failed to resolve all dependencies automatically");
+
+            Binder.withCleanCallingIdentity(() -> {
+                onError(mCallback, "Failed to resolve all dependencies automatically");
+            });
         }
 
         private ArraySet<Integer> validateSessionIds(int[] sessionIds) {
@@ -369,7 +394,8 @@
                 // Continue waiting if session exists and hasn't passed or failed yet.
                 if (sessionInfo != null) {
                     if (sessionInfo.isSessionFailed) {
-                        throwValidationError("Session already finished: " + sessionId);
+                        throw new IllegalArgumentException("Session already finished: "
+                                + sessionId);
                     }
 
                     // Wait for session to finish install if it's not already successful.
@@ -398,25 +424,17 @@
                         s -> s.sessionId == sessionId).findFirst().orElse(null);
 
                 if (sessionInfo == null) {
-                    throwValidationError("Failed to find session: " + sessionId);
+                    throw new IllegalArgumentException("Failed to find session: " + sessionId);
                 }
 
                 // Historical session must have been successful, otherwise throw IAE.
                 if (!sessionInfo.isSessionApplied) {
-                    throwValidationError("Session already finished: " + sessionId);
+                    throw new IllegalArgumentException("Session already finished: " + sessionId);
                 }
             }
 
             return validSessionIds;
         }
-
-        private void throwValidationError(String msg) {
-            // Allow client to invoke callback again.
-            synchronized (this) {
-                mDependencyInstallerCallbackInvoked = false;
-            }
-            throw new IllegalArgumentException(msg);
-        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 8e73023..7c4d425 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4426,9 +4426,9 @@
         int screenDisplayId = displayId < 0 ? DEFAULT_DISPLAY : displayId;
 
         float minLinearBrightness = mPowerManager.getBrightnessConstraint(
-                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
+                screenDisplayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
         float maxLinearBrightness = mPowerManager.getBrightnessConstraint(
-                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
+                screenDisplayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
         float linearBrightness = mDisplayManager.getBrightness(screenDisplayId);
 
         float gammaBrightness = BrightnessUtils.convertLinearToGamma(linearBrightness);
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index c573293..36bc0b9 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -6099,16 +6099,28 @@
             }
         }
 
-        public float getBrightnessConstraint(int constraint) {
+        @Override
+        public float getBrightnessConstraint(
+                int displayId, @PowerManager.BrightnessConstraint int constraint) {
+            DisplayInfo info = null;
+            if (android.companion.virtualdevice.flags.Flags.displayPowerManagerApis()
+                    && mDisplayManagerInternal != null) {
+                info = mDisplayManagerInternal.getDisplayInfo(displayId);
+            }
             switch (constraint) {
                 case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM:
-                    return mScreenBrightnessMinimum;
+                    return info != null && isValidBrightnessValue(info.brightnessMinimum)
+                            ? info.brightnessMinimum : mScreenBrightnessMinimum;
                 case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM:
-                    return mScreenBrightnessMaximum;
+                    return info != null && isValidBrightnessValue(info.brightnessMaximum)
+                            ? info.brightnessMaximum : mScreenBrightnessMaximum;
                 case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DEFAULT:
-                    return mScreenBrightnessDefault;
+                    return info != null && isValidBrightnessValue(info.brightnessDefault)
+                            ? info.brightnessDefault : mScreenBrightnessDefault;
                 case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM:
-                    return mScreenBrightnessDim;
+                    return android.companion.virtualdevice.flags.Flags.deviceAwareDisplayPower()
+                            && info != null && isValidBrightnessValue(info.brightnessDim)
+                            ? info.brightnessDim : mScreenBrightnessDim;
                 default:
                     return PowerManager.BRIGHTNESS_INVALID_FLOAT;
             }
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 4e79e37..37e8f62 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -424,6 +424,7 @@
                 || first.brightnessMinimum != second.brightnessMinimum
                 || first.brightnessMaximum != second.brightnessMaximum
                 || first.brightnessDefault != second.brightnessDefault
+                || first.brightnessDim != second.brightnessDim
                 || first.installOrientation != second.installOrientation
                 || first.isForceSdr != second.isForceSdr
                 || !Objects.equals(first.layoutLimitedRefreshRate, second.layoutLimitedRefreshRate)
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
index b002a1f..241dc10 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.display;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -327,6 +329,23 @@
     }
 
     @Test
+    public void testBrightnessConfigurationFromDisplayDevice() {
+        mDisplayDeviceInfo.brightnessMinimum = 0.12f;
+        mDisplayDeviceInfo.brightnessDim = 0.34f;
+        mDisplayDeviceInfo.brightnessDefault = 0.56f;
+        mDisplayDeviceInfo.brightnessMaximum = 0.78f;
+
+        mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice);
+        mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
+
+        DisplayInfo info = mLogicalDisplay.getDisplayInfoLocked();
+        assertThat(info.brightnessMinimum).isEqualTo(0.12f);
+        assertThat(info.brightnessDim).isEqualTo(0.34f);
+        assertThat(info.brightnessDefault).isEqualTo(0.56f);
+        assertThat(info.brightnessMaximum).isEqualTo(0.78f);
+    }
+
+    @Test
     public void testGetDisplayPosition() {
         Point expectedPosition = new Point(0, 0);
 
diff --git a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
index 3ac7fb0..dbd5c65 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.display;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
@@ -25,10 +27,12 @@
 import android.hardware.display.VirtualDisplayConfig;
 import android.media.projection.IMediaProjection;
 import android.os.IBinder;
+import android.os.PowerManager;
 import android.os.Process;
 import android.platform.test.flag.junit.CheckFlagsRule;
 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
 import android.testing.TestableContext;
+import android.view.Display;
 import android.view.Surface;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -56,6 +60,9 @@
     private static final int MAX_DEVICES = 3;
     private static final int MAX_DEVICES_PER_PACKAGE = 2;
 
+    private static final float DEFAULT_BRIGHTNESS = 0.34f;
+    private static final float DIM_BRIGHTNESS = 0.12f;
+
     @Rule
     public final TestableContext mContext = new TestableContext(
             InstrumentationRegistry.getInstrumentation().getContext());
@@ -123,6 +130,64 @@
     }
 
     @Test
+    public void testCreateVirtualDisplay_createDisplayDeviceInfoFromDefaults() {
+        VirtualDisplayConfig config = new VirtualDisplayConfig.Builder(
+                "testDisplayName", /* width= */ 640, /* height= */ 480, /* densityDpi= */ 240)
+                .build();
+
+        final String packageName = "testpackage";
+        final String displayUniqueId = VirtualDisplayAdapter.generateDisplayUniqueId(
+                packageName, Process.myUid(), config);
+
+        DisplayDevice displayDevice = mAdapter.createVirtualDisplayLocked(
+                mMockCallback, /* projection= */ null, /* ownerUid= */ 10,
+                packageName, displayUniqueId, /* surface= */ null, /* flags= */ 0, config);
+
+        assertNotNull(displayDevice);
+        DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked();
+        assertNotNull(info);
+
+        assertThat(info.width).isEqualTo(640);
+        assertThat(info.height).isEqualTo(480);
+        assertThat(info.densityDpi).isEqualTo(240);
+        assertThat(info.xDpi).isEqualTo(240);
+        assertThat(info.yDpi).isEqualTo(240);
+        assertThat(info.name).isEqualTo("testDisplayName");
+        assertThat(info.uniqueId).isEqualTo(displayUniqueId);
+        assertThat(info.ownerPackageName).isEqualTo(packageName);
+        assertThat(info.ownerUid).isEqualTo(10);
+        assertThat(info.type).isEqualTo(Display.TYPE_VIRTUAL);
+        assertThat(info.brightnessMinimum).isEqualTo(PowerManager.BRIGHTNESS_MIN);
+        assertThat(info.brightnessMaximum).isEqualTo(PowerManager.BRIGHTNESS_MAX);
+        assertThat(info.brightnessDefault).isEqualTo(PowerManager.BRIGHTNESS_MIN);
+        assertThat(info.brightnessDim).isEqualTo(PowerManager.BRIGHTNESS_INVALID);
+    }
+
+    @Test
+    public void testCreateVirtualDisplay_createDisplayDeviceInfoFromVirtualDisplayConfig() {
+        VirtualDisplayConfig config = new VirtualDisplayConfig.Builder(
+                "testDisplayName", /* width= */ 640, /* height= */ 480, /* densityDpi= */ 240)
+                .setDefaultBrightness(DEFAULT_BRIGHTNESS)
+                .setDimBrightness(DIM_BRIGHTNESS)
+                .build();
+
+        final String packageName = "testpackage";
+        final String displayUniqueId = VirtualDisplayAdapter.generateDisplayUniqueId(
+                packageName, Process.myUid(), config);
+
+        DisplayDevice displayDevice = mAdapter.createVirtualDisplayLocked(
+                mMockCallback, /* projection= */ null, /* ownerUid= */ 10,
+                packageName, displayUniqueId, /* surface= */ null, /* flags= */ 0, config);
+
+        assertNotNull(displayDevice);
+        DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked();
+        assertNotNull(info);
+
+        assertThat(info.brightnessDefault).isEqualTo(DEFAULT_BRIGHTNESS);
+        assertThat(info.brightnessDim).isEqualTo(DIM_BRIGHTNESS);
+    }
+
+    @Test
     public void testCreatesVirtualDisplay_checkGeneratedDisplayUniqueIdPrefix() {
         VirtualDisplayConfig config = new VirtualDisplayConfig.Builder("test", /* width= */ 1,
                 /* height= */ 1, /* densityDpi= */ 1).build();
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/DisplayDimModifierTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/DisplayDimModifierTest.java
index be4e7c7..7e4042e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/DisplayDimModifierTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/DisplayDimModifierTest.java
@@ -44,6 +44,8 @@
     private static final float MIN_DIM_AMOUNT = 0.05f;
     private static final float DIM_CONFIG = 0.4f;
 
+    private static final int DISPLAY_ID = 3;
+
     @Mock
     private Context mMockContext;
 
@@ -66,9 +68,9 @@
                 R.dimen.config_screenBrightnessMinimumDimAmountFloat)).thenReturn(MIN_DIM_AMOUNT);
         when(mMockContext.getSystemService(PowerManager.class)).thenReturn(mMockPowerManager);
         when(mMockPowerManager.getBrightnessConstraint(
-                PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM)).thenReturn(DIM_CONFIG);
+                DISPLAY_ID, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM)).thenReturn(DIM_CONFIG);
 
-        mModifier = new DisplayDimModifier(mMockContext);
+        mModifier = new DisplayDimModifier(DISPLAY_ID, mMockContext);
         mRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM;
     }
 
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index b48c2d7..376091e 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -1257,6 +1257,36 @@
                 .isEqualTo(WAKEFULNESS_DOZING);
     }
 
+    @EnableFlags({
+            android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER,
+            android.companion.virtualdevice.flags.Flags.FLAG_DISPLAY_POWER_MANAGER_APIS})
+    @Test
+    public void getBrightnessConstraint_valuesMatchDisplayInfo() {
+        final int displayId = 7;
+        final DisplayInfo info = new DisplayInfo();
+        info.brightnessMinimum = 0.12f;
+        info.brightnessDim = 0.34f;
+        info.brightnessDefault = 0.56f;
+        info.brightnessMaximum = 0.78f;
+        when(mDisplayManagerInternalMock.getDisplayInfo(displayId)).thenReturn(info);
+
+        createService();
+        startSystem();
+
+        assertThat(mService.getBinderServiceInstance().getBrightnessConstraint(
+                displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM))
+                .isEqualTo(info.brightnessMinimum);
+        assertThat(mService.getBinderServiceInstance().getBrightnessConstraint(
+                displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM))
+                .isEqualTo(info.brightnessMaximum);
+        assertThat(mService.getBinderServiceInstance().getBrightnessConstraint(
+                displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DEFAULT))
+                .isEqualTo(info.brightnessDefault);
+        assertThat(mService.getBinderServiceInstance().getBrightnessConstraint(
+                displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM))
+                .isEqualTo(info.brightnessDim);
+    }
+
     @SuppressWarnings("GuardedBy")
     @Test
     public void testAmbientSuppression_disablesDreamingAndWakesDevice() {
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index bc03c23..9db76d4 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -523,12 +523,12 @@
     }
 
     void prepareBrightnessDecrease(float currentBrightness) {
-        doReturn(0.0f).when(mPowerManager)
-                .getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
-        doReturn(1.0f).when(mPowerManager)
-                .getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
+        doReturn(0.0f).when(mPowerManager).getBrightnessConstraint(
+                DEFAULT_DISPLAY, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
+        doReturn(1.0f).when(mPowerManager).getBrightnessConstraint(
+                DEFAULT_DISPLAY, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
         doReturn(currentBrightness).when(mDisplayManager)
-                .getBrightness(0);
+                .getBrightness(DEFAULT_DISPLAY);
     }
 
     void verifyNewBrightness(float newBrightness) {