Merge "Post view-drawing so that it happens after applying start transaction" into tm-qpr-dev
diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java
index b505395..8bc11cb 100644
--- a/core/java/android/hardware/display/DisplayManager.java
+++ b/core/java/android/hardware/display/DisplayManager.java
@@ -1448,5 +1448,15 @@
          * @hide
          */
         String KEY_HIGH_REFRESH_RATE_BLACKLIST = "high_refresh_rate_blacklist";
+
+        /**
+         * Key for the brightness throttling data as a String formatted:
+         * <displayId>,<no of throttling levels>,[<severity as string>,<brightness cap>]
+         * Where the latter part is repeated for each throttling level, and the entirety is repeated
+         * for each display, separated by a semicolon.
+         * For example:
+         * 123,1,critical,0.8;456,2,moderate,0.9,critical,0.7
+         */
+        String KEY_BRIGHTNESS_THROTTLING_DATA = "brightness_throttling_data";
     }
 }
diff --git a/graphics/java/android/graphics/ImageDecoder.java b/graphics/java/android/graphics/ImageDecoder.java
index 1629b6a..239621e 100644
--- a/graphics/java/android/graphics/ImageDecoder.java
+++ b/graphics/java/android/graphics/ImageDecoder.java
@@ -40,6 +40,7 @@
 import android.graphics.drawable.NinePatchDrawable;
 import android.net.Uri;
 import android.os.Build;
+import android.os.Trace;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.DisplayMetrics;
@@ -223,13 +224,21 @@
         public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException {
             return nCreate(mData, mOffset, mLength, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "ByteArraySource{len=" + mLength + "}";
+        }
     }
 
     private static class ByteBufferSource extends Source {
         ByteBufferSource(@NonNull ByteBuffer buffer) {
             mBuffer = buffer;
+            mLength = mBuffer.limit() - mBuffer.position();
         }
+
         private final ByteBuffer mBuffer;
+        private final int mLength;
 
         @Override
         public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException {
@@ -241,6 +250,11 @@
             ByteBuffer buffer = mBuffer.slice();
             return nCreate(buffer, buffer.position(), buffer.limit(), preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "ByteBufferSource{len=" + mLength + "}";
+        }
     }
 
     private static class ContentResolverSource extends Source {
@@ -285,6 +299,16 @@
 
             return createFromAssetFileDescriptor(assetFd, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            String uri = mUri.toString();
+            if (uri.length() > 90) {
+                // We want to keep the Uri usable - usually the authority and the end is important.
+                uri = uri.substring(0, 80) + ".." + uri.substring(uri.length() - 10);
+            }
+            return "ContentResolverSource{uri=" + uri + "}";
+        }
     }
 
     @NonNull
@@ -399,6 +423,11 @@
                 return createFromStream(is, false, preferAnimation, this);
             }
         }
+
+        @Override
+        public String toString() {
+            return "InputStream{s=" + mInputStream + "}";
+        }
     }
 
     /**
@@ -444,6 +473,11 @@
                 return createFromAsset(ais, preferAnimation, this);
             }
         }
+
+        @Override
+        public String toString() {
+            return "AssetInputStream{s=" + mAssetInputStream + "}";
+        }
     }
 
     private static class ResourceSource extends Source {
@@ -485,6 +519,17 @@
 
             return createFromAsset((AssetInputStream) is, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            // Try to return a human-readable name for debugging purposes.
+            try {
+                return "Resource{name=" + mResources.getResourceName(mResId) + "}";
+            } catch (Resources.NotFoundException e) {
+                // It's ok if we don't find it, fall back to ID.
+            }
+            return "Resource{id=" + mResId + "}";
+        }
     }
 
     /**
@@ -521,6 +566,11 @@
             InputStream is = mAssets.open(mFileName);
             return createFromAsset((AssetInputStream) is, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "AssetSource{file=" + mFileName + "}";
+        }
     }
 
     private static class FileSource extends Source {
@@ -534,6 +584,11 @@
         public ImageDecoder createImageDecoder(boolean preferAnimation) throws IOException {
             return createFromFile(mFile, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "FileSource{file=" + mFile + "}";
+        }
     }
 
     private static class CallableSource extends Source {
@@ -557,6 +612,11 @@
             }
             return createFromAssetFileDescriptor(assetFd, preferAnimation, this);
         }
+
+        @Override
+        public String toString() {
+            return "CallableSource{obj=" + mCallable.toString() + "}";
+        }
     }
 
     /**
@@ -1763,61 +1823,65 @@
     @NonNull
     private static Drawable decodeDrawableImpl(@NonNull Source src,
             @Nullable OnHeaderDecodedListener listener) throws IOException {
+        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeDrawable");
         try (ImageDecoder decoder = src.createImageDecoder(true /*preferAnimation*/)) {
             decoder.mSource = src;
             decoder.callHeaderDecoded(listener, src);
 
-            if (decoder.mUnpremultipliedRequired) {
-                // Though this could be supported (ignored) for opaque images,
-                // it seems better to always report this error.
-                throw new IllegalStateException("Cannot decode a Drawable " +
-                                                "with unpremultiplied pixels!");
-            }
-
-            if (decoder.mMutable) {
-                throw new IllegalStateException("Cannot decode a mutable " +
-                                                "Drawable!");
-            }
-
-            // this call potentially manipulates the decoder so it must be performed prior to
-            // decoding the bitmap and after decode set the density on the resulting bitmap
-            final int srcDensity = decoder.computeDensity(src);
-            if (decoder.mAnimated) {
-                // AnimatedImageDrawable calls postProcessAndRelease only if
-                // mPostProcessor exists.
-                ImageDecoder postProcessPtr = decoder.mPostProcessor == null ?
-                        null : decoder;
-                decoder.checkState(true);
-                Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
-                        postProcessPtr, decoder.mDesiredWidth,
-                        decoder.mDesiredHeight, decoder.getColorSpacePtr(),
-                        decoder.checkForExtended(), srcDensity,
-                        src.computeDstDensity(), decoder.mCropRect,
-                        decoder.mInputStream, decoder.mAssetFd);
-                // d has taken ownership of these objects.
-                decoder.mInputStream = null;
-                decoder.mAssetFd = null;
-                return d;
-            }
-
-            Bitmap bm = decoder.decodeBitmapInternal();
-            bm.setDensity(srcDensity);
-
-            Resources res = src.getResources();
-            byte[] np = bm.getNinePatchChunk();
-            if (np != null && NinePatch.isNinePatchChunk(np)) {
-                Rect opticalInsets = new Rect();
-                bm.getOpticalInsets(opticalInsets);
-                Rect padding = decoder.mOutPaddingRect;
-                if (padding == null) {
-                    padding = new Rect();
+            try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) {
+                if (decoder.mUnpremultipliedRequired) {
+                    // Though this could be supported (ignored) for opaque images,
+                    // it seems better to always report this error.
+                    throw new IllegalStateException(
+                            "Cannot decode a Drawable with unpremultiplied pixels!");
                 }
-                nGetPadding(decoder.mNativePtr, padding);
-                return new NinePatchDrawable(res, bm, np, padding,
-                        opticalInsets, null);
-            }
 
-            return new BitmapDrawable(res, bm);
+                if (decoder.mMutable) {
+                    throw new IllegalStateException("Cannot decode a mutable Drawable!");
+                }
+
+                // this call potentially manipulates the decoder so it must be performed prior to
+                // decoding the bitmap and after decode set the density on the resulting bitmap
+                final int srcDensity = decoder.computeDensity(src);
+                if (decoder.mAnimated) {
+                    // AnimatedImageDrawable calls postProcessAndRelease only if
+                    // mPostProcessor exists.
+                    ImageDecoder postProcessPtr = decoder.mPostProcessor == null ? null : decoder;
+                    decoder.checkState(true);
+                    Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
+                            postProcessPtr, decoder.mDesiredWidth,
+                            decoder.mDesiredHeight, decoder.getColorSpacePtr(),
+                            decoder.checkForExtended(), srcDensity,
+                            src.computeDstDensity(), decoder.mCropRect,
+                            decoder.mInputStream, decoder.mAssetFd);
+                    // d has taken ownership of these objects.
+                    decoder.mInputStream = null;
+                    decoder.mAssetFd = null;
+                    return d;
+                }
+
+                Bitmap bm = decoder.decodeBitmapInternal();
+                bm.setDensity(srcDensity);
+
+                Resources res = src.getResources();
+                byte[] np = bm.getNinePatchChunk();
+                if (np != null && NinePatch.isNinePatchChunk(np)) {
+                    Rect opticalInsets = new Rect();
+                    bm.getOpticalInsets(opticalInsets);
+                    Rect padding = decoder.mOutPaddingRect;
+                    if (padding == null) {
+                        padding = new Rect();
+                    }
+                    nGetPadding(decoder.mNativePtr, padding);
+                    return new NinePatchDrawable(res, bm, np, padding,
+                            opticalInsets, null);
+                }
+
+                return new BitmapDrawable(res, bm);
+            }
+        } finally {
+            // Close the ImageDecoder#decode trace.
+            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
         }
     }
 
@@ -1867,26 +1931,51 @@
     @NonNull
     private static Bitmap decodeBitmapImpl(@NonNull Source src,
             @Nullable OnHeaderDecodedListener listener) throws IOException {
+        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ImageDecoder#decodeBitmap");
         try (ImageDecoder decoder = src.createImageDecoder(false /*preferAnimation*/)) {
             decoder.mSource = src;
             decoder.callHeaderDecoded(listener, src);
+            try (ImageDecoderSourceTrace unused = new ImageDecoderSourceTrace(decoder)) {
+                // this call potentially manipulates the decoder so it must be performed prior to
+                // decoding the bitmap
+                final int srcDensity = decoder.computeDensity(src);
+                Bitmap bm = decoder.decodeBitmapInternal();
+                bm.setDensity(srcDensity);
 
-            // this call potentially manipulates the decoder so it must be performed prior to
-            // decoding the bitmap
-            final int srcDensity = decoder.computeDensity(src);
-            Bitmap bm = decoder.decodeBitmapInternal();
-            bm.setDensity(srcDensity);
+                Rect padding = decoder.mOutPaddingRect;
+                if (padding != null) {
+                    byte[] np = bm.getNinePatchChunk();
+                    if (np != null && NinePatch.isNinePatchChunk(np)) {
+                        nGetPadding(decoder.mNativePtr, padding);
+                    }
+                }
+                return bm;
+            }
+        } finally {
+            // Close the ImageDecoder#decode trace.
+            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+        }
+    }
 
-            Rect padding = decoder.mOutPaddingRect;
-            if (padding != null) {
-                byte[] np = bm.getNinePatchChunk();
-                if (np != null && NinePatch.isNinePatchChunk(np)) {
-                    nGetPadding(decoder.mNativePtr, padding);
+    /**
+     * This describes the decoder in traces to ease debugging. It has to be called after
+     * header has been decoded and width/height have been populated. It should be used
+     * inside a try-with-resources call to automatically complete the trace.
+     */
+    private static AutoCloseable traceDecoderSource(ImageDecoder decoder) {
+        final boolean resourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES);
+        if (resourceTracingEnabled) {
+            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder));
+        }
+
+        return new AutoCloseable() {
+            @Override
+            public void close() throws Exception {
+                if (resourceTracingEnabled) {
+                    Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
                 }
             }
-
-            return bm;
-        }
+        };
     }
 
     // This method may modify the decoder so it must be called prior to performing the decode
@@ -1994,6 +2083,66 @@
         }
     }
 
+    /**
+     * Returns a short string describing what passed ImageDecoder is loading -
+     * it reports image dimensions, desired dimensions (if any) and source resource.
+     *
+     * The string appears in perf traces to simplify search for slow or memory intensive
+     * image loads.
+     *
+     * Example: ID#w=300;h=250;dw=150;dh=150;src=Resource{name=@resource}
+     *
+     * @hide
+     */
+    private static String describeDecoderForTrace(@NonNull ImageDecoder decoder) {
+        StringBuilder builder = new StringBuilder();
+        // Source dimensions
+        builder.append("ID#w=");
+        builder.append(decoder.mWidth);
+        builder.append(";h=");
+        builder.append(decoder.mHeight);
+        // Desired dimensions (if present)
+        if (decoder.mDesiredWidth != decoder.mWidth
+                || decoder.mDesiredHeight != decoder.mHeight) {
+            builder.append(";dw=");
+            builder.append(decoder.mDesiredWidth);
+            builder.append(";dh=");
+            builder.append(decoder.mDesiredHeight);
+        }
+        // Source description
+        builder.append(";src=");
+        builder.append(decoder.mSource);
+        return builder.toString();
+    }
+
+    /**
+     * Records a trace with information about the source being decoded - dimensions,
+     * desired dimensions and source information.
+     *
+     * It significantly eases debugging of slow resource loads on main thread and
+     * possible large memory consumers.
+     *
+     * @hide
+     */
+    private static final class ImageDecoderSourceTrace implements AutoCloseable {
+
+        private final boolean mResourceTracingEnabled;
+
+        ImageDecoderSourceTrace(ImageDecoder decoder) {
+            mResourceTracingEnabled = Trace.isTagEnabled(Trace.TRACE_TAG_RESOURCES);
+            if (mResourceTracingEnabled) {
+                Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, describeDecoderForTrace(decoder));
+            }
+        }
+
+        @Override
+        public void close() {
+            if (mResourceTracingEnabled) {
+                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
+            }
+        }
+    }
+
     private static native ImageDecoder nCreate(long asset,
             boolean preferAnimation, Source src) throws IOException;
     private static native ImageDecoder nCreate(ByteBuffer buffer, int position, int limit,
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index b15dce7..41791af 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -24,9 +24,9 @@
 import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceholderRule;
 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent;
 import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked;
-import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions;
+import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
 import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair;
-import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
+import static androidx.window.extensions.embedding.SplitPresenter.getNonEmbeddedActivityBounds;
 import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide;
 
 import android.app.Activity;
@@ -381,6 +381,7 @@
      *         in a state that the caller shouldn't handle.
      */
     @VisibleForTesting
+    @GuardedBy("mLock")
     boolean resolveActivityToContainer(@NonNull Activity activity, boolean isOnReparent) {
         if (isInPictureInPicture(activity) || activity.isFinishing()) {
             // We don't embed activity when it is in PIP, or finishing. Return true since we don't
@@ -581,8 +582,9 @@
     }
 
     /** Finds the activity below the given activity. */
+    @VisibleForTesting
     @Nullable
-    private Activity findActivityBelow(@NonNull Activity activity) {
+    Activity findActivityBelow(@NonNull Activity activity) {
         Activity activityBelow = null;
         final TaskFragmentContainer container = getContainerWithActivity(activity);
         if (container != null) {
@@ -606,6 +608,7 @@
      * Checks if there is a rule to split the two activities. If there is one, puts them into split
      * and returns {@code true}. Otherwise, returns {@code false}.
      */
+    @GuardedBy("mLock")
     private boolean putActivitiesIntoSplitIfNecessary(@NonNull Activity primaryActivity,
             @NonNull Activity secondaryActivity) {
         final SplitPairRule splitRule = getSplitRule(primaryActivity, secondaryActivity);
@@ -616,25 +619,25 @@
                 primaryActivity);
         final SplitContainer splitContainer = getActiveSplitForContainer(primaryContainer);
         if (splitContainer != null && primaryContainer == splitContainer.getPrimaryContainer()
-                && canReuseContainer(splitRule, splitContainer.getSplitRule())
-                && !boundsSmallerThanMinDimensions(primaryContainer.getLastRequestedBounds(),
-                        getMinDimensions(primaryActivity))) {
+                && canReuseContainer(splitRule, splitContainer.getSplitRule())) {
             // Can launch in the existing secondary container if the rules share the same
             // presentation.
             final TaskFragmentContainer secondaryContainer = splitContainer.getSecondaryContainer();
-            if (secondaryContainer == getContainerWithActivity(secondaryActivity)
-                    && !boundsSmallerThanMinDimensions(secondaryContainer.getLastRequestedBounds(),
-                            getMinDimensions(secondaryActivity))) {
+            if (secondaryContainer == getContainerWithActivity(secondaryActivity)) {
                 // The activity is already in the target TaskFragment.
                 return true;
             }
             secondaryContainer.addPendingAppearedActivity(secondaryActivity);
             final WindowContainerTransaction wct = new WindowContainerTransaction();
-            wct.reparentActivityToTaskFragment(
-                    secondaryContainer.getTaskFragmentToken(),
-                    secondaryActivity.getActivityToken());
-            mPresenter.applyTransaction(wct);
-            return true;
+            if (mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
+                    secondaryActivity, null /* secondaryIntent */)
+                    != RESULT_EXPAND_FAILED_NO_TF_INFO) {
+                wct.reparentActivityToTaskFragment(
+                        secondaryContainer.getTaskFragmentToken(),
+                        secondaryActivity.getActivityToken());
+                mPresenter.applyTransaction(wct);
+                return true;
+            }
         }
         // Create new split pair.
         mPresenter.createNewSplitContainer(primaryActivity, secondaryActivity, splitRule);
@@ -792,6 +795,7 @@
      * Returns a container for the new activity intent to launch into as splitting with the primary
      * activity.
      */
+    @GuardedBy("mLock")
     @Nullable
     private TaskFragmentContainer getSecondaryContainerForSplitIfAny(
             @NonNull WindowContainerTransaction wct, @NonNull Activity primaryActivity,
@@ -805,16 +809,12 @@
         if (splitContainer != null && existingContainer == splitContainer.getPrimaryContainer()
                 && (canReuseContainer(splitRule, splitContainer.getSplitRule())
                 // TODO(b/231845476) we should always respect clearTop.
-                || !respectClearTop)) {
-            final Rect secondaryBounds = splitContainer.getSecondaryContainer()
-                    .getLastRequestedBounds();
-            if (secondaryBounds.isEmpty()
-                    || !boundsSmallerThanMinDimensions(secondaryBounds,
-                            getMinDimensions(intent))) {
-                // Can launch in the existing secondary container if the rules share the same
-                // presentation.
-                return splitContainer.getSecondaryContainer();
-            }
+                || !respectClearTop)
+                && mPresenter.expandSplitContainerIfNeeded(wct, splitContainer, primaryActivity,
+                        null /* secondaryActivity */, intent) != RESULT_EXPAND_FAILED_NO_TF_INFO) {
+            // Can launch in the existing secondary container if the rules share the same
+            // presentation.
+            return splitContainer.getSecondaryContainer();
         }
         // Create a new TaskFragment to split with the primary activity for the new activity.
         return mPresenter.createNewSplitWithEmptySideContainer(wct, primaryActivity, intent,
@@ -868,6 +868,7 @@
      *                                  if needed.
      * @param taskId                    parent Task of the new TaskFragment.
      */
+    @GuardedBy("mLock")
     TaskFragmentContainer newContainer(@Nullable Activity pendingAppearedActivity,
             @Nullable Intent pendingAppearedIntent, @NonNull Activity activityInTask, int taskId) {
         if (activityInTask == null) {
@@ -881,7 +882,7 @@
                 pendingAppearedIntent, taskContainer, this);
         if (!taskContainer.isTaskBoundsInitialized()) {
             // Get the initial bounds before the TaskFragment has appeared.
-            final Rect taskBounds = SplitPresenter.getTaskBoundsFromActivity(activityInTask);
+            final Rect taskBounds = getNonEmbeddedActivityBounds(activityInTask);
             if (!taskContainer.setTaskBounds(taskBounds)) {
                 Log.w(TAG, "Can't find bounds from activity=" + activityInTask);
             }
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 1b79ad9..a89847a 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -65,6 +65,41 @@
     })
     private @interface Position {}
 
+    /**
+     * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
+     * Activity, Activity, Intent)}.
+     * No need to expand the splitContainer because screen is big enough to
+     * {@link #shouldShowSideBySide(Rect, SplitRule, Pair)} and minimum dimensions is satisfied.
+     */
+    static final int RESULT_NOT_EXPANDED = 0;
+    /**
+     * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
+     * Activity, Activity, Intent)}.
+     * The splitContainer should be expanded. It is usually because minimum dimensions is not
+     * satisfied.
+     * @see #shouldShowSideBySide(Rect, SplitRule, Pair)
+     */
+    static final int RESULT_EXPANDED = 1;
+    /**
+     * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
+     * Activity, Activity, Intent)}.
+     * The splitContainer should be expanded, but the client side hasn't received
+     * {@link android.window.TaskFragmentInfo} yet. Fallback to create new expanded SplitContainer
+     * instead.
+     */
+    static final int RESULT_EXPAND_FAILED_NO_TF_INFO = 2;
+
+    /**
+     * Result of {@link #expandSplitContainerIfNeeded(WindowContainerTransaction, SplitContainer,
+     * Activity, Activity, Intent)}
+     */
+    @IntDef(value = {
+            RESULT_NOT_EXPANDED,
+            RESULT_EXPANDED,
+            RESULT_EXPAND_FAILED_NO_TF_INFO,
+    })
+    private @interface ResultCode {}
+
     private final SplitController mController;
 
     SplitPresenter(@NonNull Executor executor, SplitController controller) {
@@ -396,6 +431,44 @@
         super.updateWindowingMode(wct, fragmentToken, windowingMode);
     }
 
+    /**
+     * Expands the split container if the current split bounds are smaller than the Activity or
+     * Intent that is added to the container.
+     *
+     * @return the {@link ResultCode} based on {@link #shouldShowSideBySide(Rect, SplitRule, Pair)}
+     * and if {@link android.window.TaskFragmentInfo} has reported to the client side.
+     */
+    @ResultCode
+    int expandSplitContainerIfNeeded(@NonNull WindowContainerTransaction wct,
+            @NonNull SplitContainer splitContainer, @NonNull Activity primaryActivity,
+            @Nullable Activity secondaryActivity, @Nullable Intent secondaryIntent) {
+        if (secondaryActivity == null && secondaryIntent == null) {
+            throw new IllegalArgumentException("Either secondaryActivity or secondaryIntent must be"
+                    + " non-null.");
+        }
+        final Rect taskBounds = getParentContainerBounds(primaryActivity);
+        final Pair<Size, Size> minDimensionsPair;
+        if (secondaryActivity != null) {
+            minDimensionsPair = getActivitiesMinDimensionsPair(primaryActivity, secondaryActivity);
+        } else {
+            minDimensionsPair = getActivityIntentMinDimensionsPair(primaryActivity,
+                    secondaryIntent);
+        }
+        // Expand the splitContainer if minimum dimensions are not satisfied.
+        if (!shouldShowSideBySide(taskBounds, splitContainer.getSplitRule(), minDimensionsPair)) {
+            // If the client side hasn't received TaskFragmentInfo yet, we can't change TaskFragment
+            // bounds. Return failure to create a new SplitContainer which fills task bounds.
+            if (splitContainer.getPrimaryContainer().getInfo() == null
+                    || splitContainer.getSecondaryContainer().getInfo() == null) {
+                return RESULT_EXPAND_FAILED_NO_TF_INFO;
+            }
+            expandTaskFragment(wct, splitContainer.getPrimaryContainer().getTaskFragmentToken());
+            expandTaskFragment(wct, splitContainer.getSecondaryContainer().getTaskFragmentToken());
+            return RESULT_EXPANDED;
+        }
+        return RESULT_NOT_EXPANDED;
+    }
+
     static boolean shouldShowSideBySide(@NonNull Rect parentBounds, @NonNull SplitRule rule) {
         return shouldShowSideBySide(parentBounds, rule, null /* minimumDimensionPair */);
     }
@@ -565,11 +638,19 @@
         if (container != null) {
             return getParentContainerBounds(container);
         }
-        return getTaskBoundsFromActivity(activity);
+        // Obtain bounds from Activity instead because the Activity hasn't been embedded yet.
+        return getNonEmbeddedActivityBounds(activity);
     }
 
+    /**
+     * Obtains the bounds from a non-embedded Activity.
+     * <p>
+     * Note that callers should use {@link #getParentContainerBounds(Activity)} instead for most
+     * cases unless we want to obtain task bounds before
+     * {@link TaskContainer#isTaskBoundsInitialized()}.
+     */
     @NonNull
-    static Rect getTaskBoundsFromActivity(@NonNull Activity activity) {
+    static Rect getNonEmbeddedActivityBounds(@NonNull Activity activity) {
         final WindowConfiguration windowConfiguration =
                 activity.getResources().getConfiguration().windowConfiguration;
         if (!activity.isInMultiWindowMode()) {
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
index 835c403..effc1a3 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/EmbeddingTestUtils.java
@@ -24,6 +24,7 @@
 import android.annotation.NonNull;
 import android.app.Activity;
 import android.content.Intent;
+import android.content.pm.ActivityInfo;
 import android.content.res.Configuration;
 import android.graphics.Point;
 import android.graphics.Rect;
@@ -57,13 +58,21 @@
     /** Creates a rule to always split the given activity and the given intent. */
     static SplitRule createSplitRule(@NonNull Activity primaryActivity,
             @NonNull Intent secondaryIntent) {
+        return createSplitRule(primaryActivity, secondaryIntent, true /* clearTop */);
+    }
+
+    /** Creates a rule to always split the given activity and the given intent. */
+    static SplitRule createSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryIntent, boolean clearTop) {
         final Pair<Activity, Intent> targetPair = new Pair<>(primaryActivity, secondaryIntent);
         return new SplitPairRule.Builder(
                 activityPair -> false,
                 targetPair::equals,
                 w -> true)
                 .setSplitRatio(SPLIT_RATIO)
-                .setShouldClearTop(true)
+                .setShouldClearTop(clearTop)
+                .setFinishPrimaryWithSecondary(DEFAULT_FINISH_PRIMARY_WITH_SECONDARY)
+                .setFinishSecondaryWithPrimary(DEFAULT_FINISH_SECONDARY_WITH_PRIMARY)
                 .build();
     }
 
@@ -75,6 +84,14 @@
                 true /* clearTop */);
     }
 
+    /** Creates a rule to always split the given activities. */
+    static SplitRule createSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity, boolean clearTop) {
+        return createSplitRule(primaryActivity, secondaryActivity,
+                DEFAULT_FINISH_PRIMARY_WITH_SECONDARY, DEFAULT_FINISH_SECONDARY_WITH_PRIMARY,
+                clearTop);
+    }
+
     /** Creates a rule to always split the given activities with the given finish behaviors. */
     static SplitRule createSplitRule(@NonNull Activity primaryActivity,
             @NonNull Activity secondaryActivity, int finishPrimaryWithSecondary,
@@ -105,4 +122,12 @@
                 false /* isTaskFragmentClearedForPip */,
                 new Point());
     }
+
+    static ActivityInfo createActivityInfoWithMinDimensions() {
+        ActivityInfo aInfo = new ActivityInfo();
+        final Rect primaryBounds = getSplitBounds(true /* isPrimary */);
+        aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0,
+                primaryBounds.width() + 1, primaryBounds.height() + 1);
+        return aInfo;
+    }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index ef7728c..042547f 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -22,6 +22,7 @@
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.SPLIT_RATIO;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
@@ -34,6 +35,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
@@ -436,6 +438,50 @@
     }
 
     @Test
+    public void testResolveStartActivityIntent_shouldExpandSplitContainer() {
+        final Intent intent = new Intent().setComponent(
+                new ComponentName(ApplicationProvider.getApplicationContext(),
+                        MinimumDimensionActivity.class));
+        setupSplitRule(mActivity, intent, false /* clearTop */);
+        final Activity secondaryActivity = createMockActivity();
+        addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */);
+
+        final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+                mTransaction, TASK_ID, intent, mActivity);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container));
+        assertTrue(primaryContainer.areLastRequestedBoundsEqual(null));
+        assertTrue(container.areLastRequestedBoundsEqual(null));
+        assertEquals(container, mSplitController.getContainerWithActivity(secondaryActivity));
+    }
+
+    @Test
+    public void testResolveStartActivityIntent_noInfo_shouldCreateSplitContainer() {
+        final Intent intent = new Intent().setComponent(
+                new ComponentName(ApplicationProvider.getApplicationContext(),
+                        MinimumDimensionActivity.class));
+        setupSplitRule(mActivity, intent, false /* clearTop */);
+        final Activity secondaryActivity = createMockActivity();
+        addSplitTaskFragments(mActivity, secondaryActivity, false /* clearTop */);
+
+        final TaskFragmentContainer secondaryContainer = mSplitController
+                .getContainerWithActivity(secondaryActivity);
+        secondaryContainer.mInfo = null;
+
+        final TaskFragmentContainer container = mSplitController.resolveStartActivityIntent(
+                mTransaction, TASK_ID, intent, mActivity);
+        final TaskFragmentContainer primaryContainer = mSplitController.getContainerWithActivity(
+                mActivity);
+
+        assertNotNull(mSplitController.getActiveSplitForContainers(primaryContainer, container));
+        assertTrue(primaryContainer.areLastRequestedBoundsEqual(null));
+        assertTrue(container.areLastRequestedBoundsEqual(null));
+        assertNotEquals(container, secondaryContainer);
+    }
+
+    @Test
     public void testPlaceActivityInTopContainer() {
         mSplitController.placeActivityInTopContainer(mActivity);
 
@@ -787,11 +833,7 @@
         final Activity activityBelow = createMockActivity();
         setupSplitRule(mActivity, activityBelow);
 
-        ActivityInfo aInfo = new ActivityInfo();
-        final Rect primaryBounds = getSplitBounds(true /* isPrimary */);
-        aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0,
-                primaryBounds.width() + 1, primaryBounds.height() + 1);
-        doReturn(aInfo).when(mActivity).getActivityInfo();
+        doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo();
 
         final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
                 TASK_ID);
@@ -810,17 +852,12 @@
         final Activity activityBelow = createMockActivity();
         setupSplitRule(activityBelow, mActivity);
 
-        ActivityInfo aInfo = new ActivityInfo();
-        final Rect secondaryBounds = getSplitBounds(false /* isPrimary */);
-        aInfo.windowLayout = new ActivityInfo.WindowLayout(0, 0, 0, 0, 0,
-                secondaryBounds.width() + 1, secondaryBounds.height() + 1);
-        doReturn(aInfo).when(mActivity).getActivityInfo();
+        doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo();
 
         final TaskFragmentContainer container = mSplitController.newContainer(activityBelow,
                 TASK_ID);
         container.addPendingAppearedActivity(mActivity);
 
-        // Allow to split as primary.
         boolean result = mSplitController.resolveActivityToContainer(mActivity,
                 false /* isOnReparent */);
 
@@ -828,6 +865,29 @@
         assertSplitPair(activityBelow, mActivity, true /* matchParentBounds */);
     }
 
+    // Suppress GuardedBy warning on unit tests
+    @SuppressWarnings("GuardedBy")
+    @Test
+    public void testResolveActivityToContainer_minDimensions_shouldExpandSplitContainer() {
+        final Activity primaryActivity = createMockActivity();
+        final Activity secondaryActivity = createMockActivity();
+        addSplitTaskFragments(primaryActivity, secondaryActivity, false /* clearTop */);
+
+        setupSplitRule(primaryActivity, mActivity, false /* clearTop */);
+        doReturn(createActivityInfoWithMinDimensions()).when(mActivity).getActivityInfo();
+        doReturn(secondaryActivity).when(mSplitController).findActivityBelow(eq(mActivity));
+
+        clearInvocations(mSplitPresenter);
+        boolean result = mSplitController.resolveActivityToContainer(mActivity,
+                false /* isOnReparent */);
+
+        assertTrue(result);
+        assertSplitPair(primaryActivity, mActivity, true /* matchParentBounds */);
+        assertEquals(mSplitController.getContainerWithActivity(secondaryActivity),
+                mSplitController.getContainerWithActivity(mActivity));
+        verify(mSplitPresenter, never()).createNewSplitContainer(any(), any(), any());
+    }
+
     @Test
     public void testResolveActivityToContainer_inUnknownTaskFragment() {
         doReturn(new Binder()).when(mSplitController).getInitialTaskFragmentToken(mActivity);
@@ -944,23 +1004,41 @@
     /** Setups a rule to always split the given activities. */
     private void setupSplitRule(@NonNull Activity primaryActivity,
             @NonNull Intent secondaryIntent) {
-        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent);
+        setupSplitRule(primaryActivity, secondaryIntent, true /* clearTop */);
+    }
+
+    /** Setups a rule to always split the given activities. */
+    private void setupSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Intent secondaryIntent, boolean clearTop) {
+        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryIntent, clearTop);
         mSplitController.setEmbeddingRules(Collections.singleton(splitRule));
     }
 
     /** Setups a rule to always split the given activities. */
     private void setupSplitRule(@NonNull Activity primaryActivity,
             @NonNull Activity secondaryActivity) {
-        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity);
+        setupSplitRule(primaryActivity, secondaryActivity, true /* clearTop */);
+    }
+
+    /** Setups a rule to always split the given activities. */
+    private void setupSplitRule(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity, boolean clearTop) {
+        final SplitRule splitRule = createSplitRule(primaryActivity, secondaryActivity, clearTop);
         mSplitController.setEmbeddingRules(Collections.singleton(splitRule));
     }
 
     /** Adds a pair of TaskFragments as split for the given activities. */
     private void addSplitTaskFragments(@NonNull Activity primaryActivity,
             @NonNull Activity secondaryActivity) {
+        addSplitTaskFragments(primaryActivity, secondaryActivity, true /* clearTop */);
+    }
+
+    /** Adds a pair of TaskFragments as split for the given activities. */
+    private void addSplitTaskFragments(@NonNull Activity primaryActivity,
+            @NonNull Activity secondaryActivity, boolean clearTop) {
         registerSplitPair(createMockTaskFragmentContainer(primaryActivity),
                 createMockTaskFragmentContainer(secondaryActivity),
-                createSplitRule(primaryActivity, secondaryActivity));
+                createSplitRule(primaryActivity, secondaryActivity, clearTop));
     }
 
     /** Registers the two given TaskFragments as split pair. */
@@ -1011,16 +1089,18 @@
         if (primaryContainer.mInfo != null) {
             final Rect primaryBounds = matchParentBounds ? new Rect()
                     : getSplitBounds(true /* isPrimary */);
+            final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED
+                    : WINDOWING_MODE_MULTI_WINDOW;
             assertTrue(primaryContainer.areLastRequestedBoundsEqual(primaryBounds));
-            assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(
-                    WINDOWING_MODE_MULTI_WINDOW));
+            assertTrue(primaryContainer.isLastRequestedWindowingModeEqual(windowingMode));
         }
         if (secondaryContainer.mInfo != null) {
             final Rect secondaryBounds = matchParentBounds ? new Rect()
                     : getSplitBounds(false /* isPrimary */);
+            final int windowingMode = matchParentBounds ? WINDOWING_MODE_UNDEFINED
+                    : WINDOWING_MODE_MULTI_WINDOW;
             assertTrue(secondaryContainer.areLastRequestedBoundsEqual(secondaryBounds));
-            assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(
-                    WINDOWING_MODE_MULTI_WINDOW));
+            assertTrue(secondaryContainer.isLastRequestedWindowingModeEqual(windowingMode));
         }
     }
 }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
index acc398a..d7931966 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitPresenterTest.java
@@ -20,11 +20,16 @@
 
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createActivityInfoWithMinDimensions;
+import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitRule;
 import static androidx.window.extensions.embedding.EmbeddingTestUtils.getSplitBounds;
 import static androidx.window.extensions.embedding.SplitPresenter.POSITION_END;
 import static androidx.window.extensions.embedding.SplitPresenter.POSITION_FILL;
 import static androidx.window.extensions.embedding.SplitPresenter.POSITION_START;
+import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPANDED;
+import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO;
+import static androidx.window.extensions.embedding.SplitPresenter.RESULT_NOT_EXPANDED;
 import static androidx.window.extensions.embedding.SplitPresenter.getBoundsForPosition;
 import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions;
 import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSideBySide;
@@ -34,6 +39,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -49,6 +55,7 @@
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Rect;
+import android.os.IBinder;
 import android.platform.test.annotations.Presubmit;
 import android.util.Pair;
 import android.util.Size;
@@ -195,6 +202,52 @@
                         splitRule, mActivity, minDimensionsPair));
     }
 
+    @Test
+    public void testExpandSplitContainerIfNeeded() {
+        SplitContainer splitContainer = mock(SplitContainer.class);
+        Activity secondaryActivity = createMockActivity();
+        SplitRule splitRule = createSplitRule(mActivity, secondaryActivity);
+        TaskFragmentContainer primaryTf = mController.newContainer(mActivity, TASK_ID);
+        TaskFragmentContainer secondaryTf = mController.newContainer(secondaryActivity, TASK_ID);
+        doReturn(splitRule).when(splitContainer).getSplitRule();
+        doReturn(primaryTf).when(splitContainer).getPrimaryContainer();
+        doReturn(secondaryTf).when(splitContainer).getSecondaryContainer();
+
+        assertThrows(IllegalArgumentException.class, () ->
+                mPresenter.expandSplitContainerIfNeeded(mTransaction, splitContainer, mActivity,
+                        null /* secondaryActivity */, null /* secondaryIntent */));
+
+        assertEquals(RESULT_NOT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
+                splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
+        verify(mPresenter, never()).expandTaskFragment(any(), any());
+
+        doReturn(createActivityInfoWithMinDimensions()).when(secondaryActivity).getActivityInfo();
+        assertEquals(RESULT_EXPAND_FAILED_NO_TF_INFO, mPresenter.expandSplitContainerIfNeeded(
+                mTransaction, splitContainer, mActivity, secondaryActivity,
+                null /* secondaryIntent */));
+
+        primaryTf.setInfo(createMockTaskFragmentInfo(primaryTf, mActivity));
+        secondaryTf.setInfo(createMockTaskFragmentInfo(secondaryTf, secondaryActivity));
+
+        assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
+                splitContainer, mActivity, secondaryActivity, null /* secondaryIntent */));
+        verify(mPresenter).expandTaskFragment(eq(mTransaction),
+                eq(primaryTf.getTaskFragmentToken()));
+        verify(mPresenter).expandTaskFragment(eq(mTransaction),
+                eq(secondaryTf.getTaskFragmentToken()));
+
+        clearInvocations(mPresenter);
+
+        assertEquals(RESULT_EXPANDED, mPresenter.expandSplitContainerIfNeeded(mTransaction,
+                splitContainer, mActivity, null /* secondaryActivity */,
+                new Intent(ApplicationProvider.getApplicationContext(),
+                        MinimumDimensionActivity.class)));
+        verify(mPresenter).expandTaskFragment(eq(mTransaction),
+                eq(primaryTf.getTaskFragmentToken()));
+        verify(mPresenter).expandTaskFragment(eq(mTransaction),
+                eq(secondaryTf.getTaskFragmentToken()));
+    }
+
     private Activity createMockActivity() {
         final Activity activity = mock(Activity.class);
         final Configuration activityConfig = new Configuration();
@@ -203,6 +256,7 @@
         doReturn(mActivityResources).when(activity).getResources();
         doReturn(activityConfig).when(mActivityResources).getConfiguration();
         doReturn(new ActivityInfo()).when(activity).getActivityInfo();
+        doReturn(mock(IBinder.class)).when(activity).getActivityToken();
         return activity;
     }
 }
diff --git a/libs/WindowManager/Shell/res/values-television/config.xml b/libs/WindowManager/Shell/res/values-television/config.xml
index 86ca655..cc0333e 100644
--- a/libs/WindowManager/Shell/res/values-television/config.xml
+++ b/libs/WindowManager/Shell/res/values-television/config.xml
@@ -43,4 +43,13 @@
     <!-- Time (duration in milliseconds) that the shell waits for an app to close the PiP by itself
     if a custom action is present before closing it. -->
     <integer name="config_pipForceCloseDelay">5000</integer>
+
+    <!-- Animation duration when exit starting window: fade out icon -->
+    <integer name="starting_window_app_reveal_icon_fade_out_duration">0</integer>
+
+    <!-- Animation duration when exit starting window: reveal app -->
+    <integer name="starting_window_app_reveal_anim_delay">0</integer>
+
+    <!-- Animation duration when exit starting window: reveal app -->
+    <integer name="starting_window_app_reveal_anim_duration">0</integer>
 </resources>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index 89e10c4..fc70ba4 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -20,15 +20,19 @@
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothUuid;
+import android.os.Build;
 import android.os.ParcelUuid;
 import android.util.Log;
 
+import androidx.annotation.ChecksSdkIntAtLeast;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 /**
  * CsipDeviceManager manages the set of remote CSIP Bluetooth devices.
@@ -126,32 +130,84 @@
         }
     }
 
+    @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.TIRAMISU)
+    private static boolean isAtLeastT() {
+        return Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU;
+    }
+
     // Group devices by groupId
     @VisibleForTesting
     void onGroupIdChanged(int groupId) {
-        int firstMatchedIndex = -1;
-        CachedBluetoothDevice mainDevice = null;
+        if (!isValidGroupId(groupId)) {
+            log("onGroupIdChanged: groupId is invalid");
+            return;
+        }
+        log("onGroupIdChanged: mCachedDevices list =" + mCachedDevices.toString());
+        final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager();
+        final CachedBluetoothDeviceManager deviceManager = mBtManager.getCachedDeviceManager();
+        final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile();
+        final BluetoothDevice mainBluetoothDevice = (leAudioProfile != null && isAtLeastT()) ?
+                leAudioProfile.getConnectedGroupLeadDevice(groupId) : null;
+        CachedBluetoothDevice newMainDevice =
+                mainBluetoothDevice != null ? deviceManager.findDevice(mainBluetoothDevice) : null;
+        if (newMainDevice != null) {
+            final CachedBluetoothDevice finalNewMainDevice = newMainDevice;
+            final List<CachedBluetoothDevice> memberDevices = mCachedDevices.stream()
+                    .filter(cachedDevice -> !cachedDevice.equals(finalNewMainDevice)
+                            && cachedDevice.getGroupId() == groupId)
+                    .collect(Collectors.toList());
+            if (memberDevices == null || memberDevices.isEmpty()) {
+                log("onGroupIdChanged: There is no member device in list.");
+                return;
+            }
+            log("onGroupIdChanged: removed from UI device =" + memberDevices
+                    + ", with groupId=" + groupId + " mainDevice= " + newMainDevice);
+            for (CachedBluetoothDevice memberDeviceItem : memberDevices) {
+                Set<CachedBluetoothDevice> memberSet = memberDeviceItem.getMemberDevice();
+                if (!memberSet.isEmpty()) {
+                    log("onGroupIdChanged: Transfer the member list into new main device.");
+                    for (CachedBluetoothDevice memberListItem : memberSet) {
+                        if (!memberListItem.equals(newMainDevice)) {
+                            newMainDevice.addMemberDevice(memberListItem);
+                        }
+                    }
+                    memberSet.clear();
+                }
 
-        for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
-            final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
-            if (cachedDevice.getGroupId() != groupId) {
-                continue;
+                newMainDevice.addMemberDevice(memberDeviceItem);
+                mCachedDevices.remove(memberDeviceItem);
+                mBtManager.getEventManager().dispatchDeviceRemoved(memberDeviceItem);
             }
 
-            if (firstMatchedIndex == -1) {
-                // Found the first one
-                firstMatchedIndex = i;
-                mainDevice = cachedDevice;
-                continue;
+            if (!mCachedDevices.contains(newMainDevice)) {
+                mCachedDevices.add(newMainDevice);
+                mBtManager.getEventManager().dispatchDeviceAdded(newMainDevice);
             }
+        } else {
+            log("onGroupIdChanged: There is no main device from the LE profile.");
+            int firstMatchedIndex = -1;
 
-            log("onGroupIdChanged: removed from UI device =" + cachedDevice
-                    + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex);
+            for (int i = mCachedDevices.size() - 1; i >= 0; i--) {
+                final CachedBluetoothDevice cachedDevice = mCachedDevices.get(i);
+                if (cachedDevice.getGroupId() != groupId) {
+                    continue;
+                }
 
-            mainDevice.addMemberDevice(cachedDevice);
-            mCachedDevices.remove(i);
-            mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
-            break;
+                if (firstMatchedIndex == -1) {
+                    // Found the first one
+                    firstMatchedIndex = i;
+                    newMainDevice = cachedDevice;
+                    continue;
+                }
+
+                log("onGroupIdChanged: removed from UI device =" + cachedDevice
+                        + ", with groupId=" + groupId + " firstMatchedIndex=" + firstMatchedIndex);
+
+                newMainDevice.addMemberDevice(cachedDevice);
+                mCachedDevices.remove(i);
+                mBtManager.getEventManager().dispatchDeviceRemoved(cachedDevice);
+                break;
+            }
         }
     }
 
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
index 19df1e9..0f57d87 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java
@@ -21,6 +21,7 @@
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_ALLOWED;
 import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN;
 
+import android.annotation.Nullable;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothCodecConfig;
@@ -183,6 +184,37 @@
         return mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO);
     }
 
+    /**
+     * Get Lead device for the group.
+     *
+     * Lead device is the device that can be used as an active device in the system.
+     * Active devices points to the Audio Device for the Le Audio group.
+     * This method returns the Lead devices for the connected LE Audio
+     * group and this device should be used in the setActiveDevice() method by other parts
+     * of the system, which wants to set to active a particular Le Audio group.
+     *
+     * Note: getActiveDevice() returns the Lead device for the currently active LE Audio group.
+     * Note: When Lead device gets disconnected while Le Audio group is active and has more devices
+     * in the group, then Lead device will not change. If Lead device gets disconnected, for the
+     * Le Audio group which is not active, a new Lead device will be chosen
+     *
+     * @param groupId The group id.
+     * @return group lead device.
+     *
+     * @hide
+     */
+    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
+    public @Nullable BluetoothDevice getConnectedGroupLeadDevice(int groupId) {
+        if (DEBUG) {
+            Log.d(TAG,"getConnectedGroupLeadDevice");
+        }
+        if (mService == null) {
+            Log.e(TAG,"No service.");
+            return null;
+        }
+        return mService.getConnectedGroupLeadDevice(groupId);
+    }
+
     @Override
     public boolean isEnabled(BluetoothDevice device) {
         if (mService == null || device == null) {
diff --git a/packages/SystemUI/res/values-h800dp/dimens.xml b/packages/SystemUI/res/values-h800dp/dimens.xml
index e6af6f4..94fe209 100644
--- a/packages/SystemUI/res/values-h800dp/dimens.xml
+++ b/packages/SystemUI/res/values-h800dp/dimens.xml
@@ -15,9 +15,6 @@
   -->
 
 <resources>
-    <!-- Minimum margin between clock and top of screen or ambient indication -->
-    <dimen name="keyguard_clock_top_margin">26dp</dimen>
-
     <!-- Large clock maximum font size (dp is intentional, to prevent any further scaling) -->
     <dimen name="large_clock_text_size">200dp</dimen>
 
diff --git a/packages/SystemUI/screenshot/Android.bp b/packages/SystemUI/screenshot/Android.bp
new file mode 100644
index 0000000..a79fd9040d
--- /dev/null
+++ b/packages/SystemUI/screenshot/Android.bp
@@ -0,0 +1,48 @@
+// Copyright (C) 2022 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 {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
+}
+
+android_library {
+    name: "SystemUIScreenshotLib",
+    manifest: "AndroidManifest.xml",
+
+    srcs: [
+        // All files in this library should be in Kotlin besides some exceptions.
+        "src/**/*.kt",
+
+        // This file was forked from google3, so exceptionally it can be in Java.
+        "src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java",
+    ],
+
+    resource_dirs: [
+        "res",
+    ],
+
+    static_libs: [
+        "SystemUI-core",
+        "androidx.test.espresso.core",
+        "androidx.appcompat_appcompat",
+        "platform-screenshot-diff-core",
+    ],
+
+    kotlincflags: ["-Xjvm-default=all"],
+}
diff --git a/packages/SystemUI/screenshot/AndroidManifest.xml b/packages/SystemUI/screenshot/AndroidManifest.xml
new file mode 100644
index 0000000..3b703be
--- /dev/null
+++ b/packages/SystemUI/screenshot/AndroidManifest.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2022 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.systemui.testing.screenshot">
+    <application>
+        <activity
+            android:name="com.android.systemui.testing.screenshot.ScreenshotActivity"
+            android:exported="true"
+            android:theme="@style/Theme.SystemUI.Screenshot" />
+    </application>
+
+    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
+</manifest>
diff --git a/packages/SystemUI/screenshot/res/values/themes.xml b/packages/SystemUI/screenshot/res/values/themes.xml
new file mode 100644
index 0000000..40e50bb
--- /dev/null
+++ b/packages/SystemUI/screenshot/res/values/themes.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2022 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.
+-->
+<resources>
+    <style name="Theme.SystemUI.Screenshot" parent="Theme.SystemUI">
+        <item name="android:windowActionBar">false</item>
+        <item name="android:windowNoTitle">true</item>
+
+        <!-- Make sure that device specific cutouts don't impact the outcome of screenshot tests -->
+        <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+    </style>
+</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java
new file mode 100644
index 0000000..96ec4c5
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/DynamicColorsTestUtils.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2022 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.testing.screenshot;
+
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+
+import android.app.UiAutomation;
+import android.content.Context;
+import android.provider.Settings;
+import android.util.Log;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.ColorRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+import androidx.core.content.ContextCompat;
+import androidx.test.espresso.Espresso;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+
+import org.json.JSONObject;
+import org.junit.function.ThrowingRunnable;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/*
+ * Note: This file was forked from
+ * google3/third_party/java_src/android_libs/material_components/screenshot_tests/java/android/
+ * support/design/scuba/color/DynamicColorsTestUtils.java.
+ */
+
+/** Utility that helps change the dynamic system colors for testing. */
+@RequiresApi(32)
+public class DynamicColorsTestUtils {
+
+    private static final String TAG = DynamicColorsTestUtils.class.getSimpleName();
+
+    private static final String THEME_CUSTOMIZATION_KEY = "theme_customization_overlay_packages";
+    private static final String THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY =
+            "android.theme.customization.system_palette";
+
+    private static final int ORANGE_SYSTEM_SEED_COLOR = 0xA66800;
+    private static final int ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR = -8235756;
+
+    private DynamicColorsTestUtils() {
+    }
+
+    /**
+     * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on an orange
+     * seed color, and then wait for the change to propagate to the app by comparing
+     * android.R.color.system_accent1_600 to the expected orange value.
+     */
+    public static void updateSystemColorsToOrange() {
+        updateSystemColors(ORANGE_SYSTEM_SEED_COLOR, ORANGE_EXPECTED_SYSTEM_ACCENT1_600_COLOR);
+    }
+
+    /**
+     * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided
+     * {@code seedColor}, and then wait for the change to propagate to the app by comparing
+     * android.R.color.system_accent1_600 to {@code expectedSystemAccent1600}.
+     */
+    public static void updateSystemColors(
+            @ColorInt int seedColor, @ColorInt int expectedSystemAccent1600) {
+        Context context = getInstrumentation().getTargetContext();
+
+        int actualSystemAccent1600 =
+                ContextCompat.getColor(context, android.R.color.system_accent1_600);
+
+        if (expectedSystemAccent1600 == actualSystemAccent1600) {
+            String expectedColorString = Integer.toHexString(expectedSystemAccent1600);
+            Log.d(
+                    TAG,
+                    "Skipped updating system colors since system_accent1_600 is already equal to "
+                            + "expected: "
+                            + expectedColorString);
+            return;
+        }
+
+        updateSystemColors(seedColor);
+    }
+
+    /**
+     * Update system dynamic colors (e.g., android.R.color.system_accent1_600) based on the provided
+     * {@code seedColor}, and then wait for the change to propagate to the app by checking
+     * android.R.color.system_accent1_600 for any change.
+     */
+    public static void updateSystemColors(@ColorInt int seedColor) {
+        Context context = getInstrumentation().getTargetContext();
+
+        // Initialize system color idling resource with original system_accent1_600 value.
+        ColorChangeIdlingResource systemColorIdlingResource =
+                new ColorChangeIdlingResource(context, android.R.color.system_accent1_600);
+
+        // Update system theme color setting to trigger fabricated resource overlay.
+        runWithShellPermissionIdentity(
+                () ->
+                        Settings.Secure.putString(
+                                context.getContentResolver(),
+                                THEME_CUSTOMIZATION_KEY,
+                                buildThemeCustomizationString(seedColor)));
+
+        // Wait for system color update to propagate to app.
+        IdlingRegistry idlingRegistry = IdlingRegistry.getInstance();
+        idlingRegistry.register(systemColorIdlingResource);
+        Espresso.onIdle();
+        idlingRegistry.unregister(systemColorIdlingResource);
+
+        Log.d(TAG,
+                Settings.Secure.getString(context.getContentResolver(), THEME_CUSTOMIZATION_KEY));
+    }
+
+    private static String buildThemeCustomizationString(@ColorInt int seedColor) {
+        String seedColorHex = Integer.toHexString(seedColor);
+        Map<String, String> themeCustomizationMap = new HashMap<>();
+        themeCustomizationMap.put(THEME_CUSTOMIZATION_SYSTEM_PALETTE_KEY, seedColorHex);
+        return new JSONObject(themeCustomizationMap).toString();
+    }
+
+    private static void runWithShellPermissionIdentity(@NonNull ThrowingRunnable runnable) {
+        UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
+        uiAutomation.adoptShellPermissionIdentity();
+        try {
+            runnable.run();
+        } catch (Throwable e) {
+            throw new RuntimeException(e);
+        } finally {
+            uiAutomation.dropShellPermissionIdentity();
+        }
+    }
+
+    private static class ColorChangeIdlingResource implements IdlingResource {
+
+        private final Context mContext;
+        private final int mColorResId;
+        private final int mInitialColorInt;
+
+        private ResourceCallback mResourceCallback;
+        private boolean mIdleNow;
+
+        ColorChangeIdlingResource(Context context, @ColorRes int colorResId) {
+            this.mContext = context;
+            this.mColorResId = colorResId;
+            this.mInitialColorInt = ContextCompat.getColor(context, colorResId);
+        }
+
+        @Override
+        public String getName() {
+            return ColorChangeIdlingResource.class.getName();
+        }
+
+        @Override
+        public boolean isIdleNow() {
+            if (mIdleNow) {
+                return true;
+            }
+
+            int currentColorInt = ContextCompat.getColor(mContext, mColorResId);
+
+            String initialColorString = Integer.toHexString(mInitialColorInt);
+            String currentColorString = Integer.toHexString(currentColorInt);
+            Log.d(TAG, String.format("Initial=%s, Current=%s", initialColorString,
+                    currentColorString));
+
+            mIdleNow = currentColorInt != mInitialColorInt;
+            Log.d(TAG, String.format("idleNow=%b", mIdleNow));
+
+            if (mIdleNow) {
+                mResourceCallback.onTransitionToIdle();
+            }
+            return mIdleNow;
+        }
+
+        @Override
+        public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
+            this.mResourceCallback = resourceCallback;
+        }
+    }
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt
new file mode 100644
index 0000000..2a55a80
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotActivity.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 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.testing.screenshot
+
+import androidx.activity.ComponentActivity
+
+/** The Activity that is launched and whose content is set for screenshot tests. */
+class ScreenshotActivity : ComponentActivity()
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt
new file mode 100644
index 0000000..363ce10
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestRule.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2022 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.testing.screenshot
+
+import android.app.UiModeManager
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.os.UserHandle
+import android.view.Display
+import android.view.View
+import android.view.WindowManagerGlobal
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+import platform.test.screenshot.GoldenImagePathManager
+import platform.test.screenshot.PathConfig
+import platform.test.screenshot.PathElementNoContext
+import platform.test.screenshot.ScreenshotTestRule
+import platform.test.screenshot.matchers.PixelPerfectMatcher
+
+/**
+ * A base rule for screenshot diff tests.
+ *
+ * This rules takes care of setting up the activity according to [testSpec] by:
+ * - emulating the display size and density.
+ * - setting the dark/light mode.
+ * - setting the system (Material You) colors to a fixed value.
+ *
+ * @see ComposeScreenshotTestRule
+ * @see ViewScreenshotTestRule
+ */
+class ScreenshotTestRule(private val testSpec: ScreenshotTestSpec) : TestRule {
+    private var currentDisplay: DisplaySpec? = null
+    private var currentGoldenIdentifier: String? = null
+
+    private val pathConfig =
+        PathConfig(
+            PathElementNoContext("model", isDir = true) {
+                currentDisplay?.name ?: error("currentDisplay is null")
+            },
+        )
+    private val defaultMatcher = PixelPerfectMatcher()
+
+    private val screenshotRule =
+        ScreenshotTestRule(
+            SystemUIGoldenImagePathManager(
+                pathConfig,
+                currentGoldenIdentifier = {
+                    currentGoldenIdentifier ?: error("currentGoldenIdentifier is null")
+                },
+            )
+        )
+
+    override fun apply(base: Statement, description: Description): Statement {
+        // The statement which call beforeTest() before running the test and afterTest() afterwards.
+        val statement =
+            object : Statement() {
+                override fun evaluate() {
+                    try {
+                        beforeTest()
+                        base.evaluate()
+                    } finally {
+                        afterTest()
+                    }
+                }
+            }
+
+        return screenshotRule.apply(statement, description)
+    }
+
+    private fun beforeTest() {
+        // Update the system colors to a fixed color, so that tests don't depend on the host device
+        // extracted colors. Note that we don't restore the default device colors at the end of the
+        // test because changing the colors (and waiting for them to be applied) is costly and makes
+        // the screenshot tests noticeably slower.
+        DynamicColorsTestUtils.updateSystemColorsToOrange()
+
+        // Emulate the display size and density.
+        val display = testSpec.display
+        val density = display.densityDpi
+        val wm = WindowManagerGlobal.getWindowManagerService()
+        val (width, height) = getEmulatedDisplaySize()
+        wm.setForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, density, UserHandle.myUserId())
+        wm.setForcedDisplaySize(Display.DEFAULT_DISPLAY, width, height)
+
+        // Force the dark/light theme.
+        val uiModeManager =
+            InstrumentationRegistry.getInstrumentation()
+                .targetContext
+                .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
+        uiModeManager.setApplicationNightMode(
+            if (testSpec.isDarkTheme) {
+                UiModeManager.MODE_NIGHT_YES
+            } else {
+                UiModeManager.MODE_NIGHT_NO
+            }
+        )
+    }
+
+    private fun afterTest() {
+        // Reset the density and display size.
+        val wm = WindowManagerGlobal.getWindowManagerService()
+        wm.clearForcedDisplayDensityForUser(Display.DEFAULT_DISPLAY, UserHandle.myUserId())
+        wm.clearForcedDisplaySize(Display.DEFAULT_DISPLAY)
+
+        // Reset the dark/light theme.
+        val uiModeManager =
+            InstrumentationRegistry.getInstrumentation()
+                .targetContext
+                .getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
+        uiModeManager.setApplicationNightMode(UiModeManager.MODE_NIGHT_AUTO)
+    }
+
+    /**
+     * Compare the content of [view] with the golden image identified by [goldenIdentifier] in the
+     * context of [testSpec].
+     */
+    fun screenshotTest(goldenIdentifier: String, view: View) {
+        val bitmap = drawIntoBitmap(view)
+
+        // Compare bitmap against golden asset.
+        val isDarkTheme = testSpec.isDarkTheme
+        val isLandscape = testSpec.isLandscape
+        val identifierWithSpec = buildString {
+            append(goldenIdentifier)
+            if (isDarkTheme) append("_dark")
+            if (isLandscape) append("_landscape")
+        }
+
+        // TODO(b/230832101): Provide a way to pass a PathConfig and override the file name on
+        // device to assertBitmapAgainstGolden instead?
+        currentDisplay = testSpec.display
+        currentGoldenIdentifier = goldenIdentifier
+        screenshotRule.assertBitmapAgainstGolden(bitmap, identifierWithSpec, defaultMatcher)
+        currentDisplay = null
+        currentGoldenIdentifier = goldenIdentifier
+    }
+
+    /** Draw [view] into a [Bitmap]. */
+    private fun drawIntoBitmap(view: View): Bitmap {
+        val bitmap =
+            Bitmap.createBitmap(
+                view.measuredWidth,
+                view.measuredHeight,
+                Bitmap.Config.ARGB_8888,
+            )
+        val canvas = Canvas(bitmap)
+        view.draw(canvas)
+        return bitmap
+    }
+
+    /** Get the emulated display size for [testSpec]. */
+    private fun getEmulatedDisplaySize(): Pair<Int, Int> {
+        val display = testSpec.display
+        val isPortraitNaturalPosition = display.width < display.height
+        return if (testSpec.isLandscape) {
+            if (isPortraitNaturalPosition) {
+                display.height to display.width
+            } else {
+                display.width to display.height
+            }
+        } else {
+            if (isPortraitNaturalPosition) {
+                display.width to display.height
+            } else {
+                display.height to display.width
+            }
+        }
+    }
+}
+
+private class SystemUIGoldenImagePathManager(
+    pathConfig: PathConfig,
+    private val currentGoldenIdentifier: () -> String,
+) :
+    GoldenImagePathManager(
+        appContext = InstrumentationRegistry.getInstrumentation().context,
+        deviceLocalPath =
+            InstrumentationRegistry.getInstrumentation()
+                .targetContext
+                .filesDir
+                .absolutePath
+                .toString() + "/sysui_screenshots",
+        pathConfig = pathConfig,
+    ) {
+    // This string is appended to all actual/expected screenshots on the device. We append the
+    // golden identifier so that our pull_golden.py scripts can map a screenshot on device to its
+    // asset (and automatically update it, if necessary).
+    override fun toString() = currentGoldenIdentifier()
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt
new file mode 100644
index 0000000..7fc6245
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ScreenshotTestSpec.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 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.testing.screenshot
+
+/** The specification of a device display to be used in a screenshot test. */
+data class DisplaySpec(
+    val name: String,
+    val width: Int,
+    val height: Int,
+    val densityDpi: Int,
+)
+
+/** The specification of a screenshot diff test. */
+class ScreenshotTestSpec(
+    val display: DisplaySpec,
+    val isDarkTheme: Boolean = false,
+    val isLandscape: Boolean = false,
+) {
+    companion object {
+        /**
+         * Return a list of [ScreenshotTestSpec] for each of the [displays].
+         *
+         * If [isDarkTheme] is null, this will create a spec for both light and dark themes, for
+         * each of the orientation.
+         *
+         * If [isLandscape] is null, this will create a spec for both portrait and landscape, for
+         * each of the light/dark themes.
+         */
+        fun forDisplays(
+            vararg displays: DisplaySpec,
+            isDarkTheme: Boolean? = null,
+            isLandscape: Boolean? = null,
+        ): List<ScreenshotTestSpec> {
+            return displays.flatMap { display ->
+                buildList {
+                    fun addDisplay(isLandscape: Boolean) {
+                        if (isDarkTheme != true) {
+                            add(ScreenshotTestSpec(display, isDarkTheme = false, isLandscape))
+                        }
+
+                        if (isDarkTheme != false) {
+                            add(ScreenshotTestSpec(display, isDarkTheme = true, isLandscape))
+                        }
+                    }
+
+                    if (isLandscape != true) {
+                        addDisplay(isLandscape = false)
+                    }
+
+                    if (isLandscape != false) {
+                        addDisplay(isLandscape = true)
+                    }
+                }
+            }
+        }
+    }
+
+    override fun toString(): String = buildString {
+        // This string is appended to PNGs stored in the device, so let's keep it simple.
+        append(display.name)
+        if (isDarkTheme) append("_dark")
+        if (isLandscape) append("_landscape")
+    }
+}
diff --git a/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
new file mode 100644
index 0000000..2c3ff2c
--- /dev/null
+++ b/packages/SystemUI/screenshot/src/com/android/systemui/testing/screenshot/ViewScreenshotTestRule.kt
@@ -0,0 +1,51 @@
+package com.android.systemui.testing.screenshot
+
+import android.app.Activity
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams
+import androidx.test.ext.junit.rules.ActivityScenarioRule
+import org.junit.Assert.assertEquals
+import org.junit.rules.RuleChain
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/** A rule for View screenshot diff tests. */
+class ViewScreenshotTestRule(testSpec: ScreenshotTestSpec) : TestRule {
+    private val activityRule = ActivityScenarioRule(ScreenshotActivity::class.java)
+    private val screenshotRule = ScreenshotTestRule(testSpec)
+
+    private val delegate = RuleChain.outerRule(screenshotRule).around(activityRule)
+
+    override fun apply(base: Statement, description: Description): Statement {
+        return delegate.apply(base, description)
+    }
+
+    /**
+     * Compare the content of [view] with the golden image identified by [goldenIdentifier] in the
+     * context of [testSpec].
+     */
+    fun screenshotTest(
+        goldenIdentifier: String,
+        layoutParams: LayoutParams =
+            LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT),
+        view: (Activity) -> View,
+    ) {
+        activityRule.scenario.onActivity { activity ->
+            // Make sure that the activity draws full screen and fits the whole display instead of
+            // the system bars.
+            activity.window.setDecorFitsSystemWindows(false)
+            activity.setContentView(view(activity), layoutParams)
+        }
+
+        // We call onActivity again because it will make sure that our Activity is done measuring,
+        // laying out and drawing its content (that we set in the previous onActivity lambda).
+        activityRule.scenario.onActivity { activity ->
+            // Check that the content is what we expected.
+            val content = activity.requireViewById<ViewGroup>(android.R.id.content)
+            assertEquals(1, content.childCount)
+            screenshotRule.screenshotTest(goldenIdentifier, content.getChildAt(0))
+        }
+    }
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
index 4ce110b..249133a 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -141,8 +141,10 @@
         final int mode = change.getMode();
 
         t.reparent(leash, info.getRootLeash());
-        t.setPosition(leash, change.getStartAbsBounds().left - info.getRootOffset().x,
-                change.getStartAbsBounds().top - info.getRootOffset().y);
+        final Rect absBounds =
+                (mode == TRANSIT_OPEN) ? change.getEndAbsBounds() : change.getStartAbsBounds();
+        t.setPosition(leash, absBounds.left - info.getRootOffset().x,
+                absBounds.top - info.getRootOffset().y);
 
         // Put all the OPEN/SHOW on top
         if (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 3f70d4e..a54f4c2 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -60,6 +60,9 @@
     public static final ResourceBooleanFlag NOTIFICATION_DRAG_TO_CONTENTS =
             new ResourceBooleanFlag(108, R.bool.config_notificationToContents);
 
+    public static final BooleanFlag REMOVE_UNRANKED_NOTIFICATIONS =
+            new BooleanFlag(109, false);
+
     /***************************************/
     // 200 - keyguard/lockscreen
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
index ed5c193..2f732de 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaHierarchyManager.kt
@@ -23,6 +23,7 @@
 import android.content.Context
 import android.content.res.Configuration
 import android.graphics.Rect
+import android.util.Log
 import android.util.MathUtils
 import android.view.View
 import android.view.ViewGroup
@@ -48,6 +49,8 @@
 import com.android.systemui.util.traceSection
 import javax.inject.Inject
 
+private val TAG: String = MediaHierarchyManager::class.java.simpleName
+
 /**
  * Similarly to isShown but also excludes views that have 0 alpha
  */
@@ -964,6 +967,14 @@
                         top,
                         left + currentBounds.width(),
                         top + currentBounds.height())
+
+                if (mediaFrame.childCount > 0) {
+                    val child = mediaFrame.getChildAt(0)
+                    if (mediaFrame.height < child.height) {
+                        Log.wtf(TAG, "mediaFrame height is too small for child: " +
+                            "${mediaFrame.height} vs ${child.height}")
+                    }
+                }
             }
             if (isCrossFadeAnimatorRunning) {
                 // When cross-fading with an animation, we only notify the media carousel of the
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 5d2060d..7b1ddd6 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -139,12 +139,11 @@
 
     void updateResources(QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController) {
-        int bottomPadding = getResources().getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
         mQSPanelContainer.setPaddingRelative(
                 mQSPanelContainer.getPaddingStart(),
                 QSUtils.getQsHeaderSystemIconsAreaHeight(mContext),
                 mQSPanelContainer.getPaddingEnd(),
-                bottomPadding);
+                mQSPanelContainer.getPaddingBottom());
 
         int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin);
         int horizontalPadding = getResources().getDimensionPixelSize(
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 41724ef..324c019 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -362,11 +362,11 @@
     protected void updatePadding() {
         final Resources res = mContext.getResources();
         int paddingTop = res.getDimensionPixelSize(R.dimen.qs_panel_padding_top);
-        // Bottom padding only when there's a new footer with its height.
+        int paddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
         setPaddingRelative(getPaddingStart(),
                 paddingTop,
                 getPaddingEnd(),
-                getPaddingBottom());
+                paddingBottom);
     }
 
     void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 478f7aa..c4947ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -56,4 +56,7 @@
     fun isSmartspaceDedupingEnabled(): Boolean =
             featureFlags.isEnabled(Flags.SMARTSPACE) &&
                     featureFlags.isEnabled(Flags.SMARTSPACE_DEDUPING)
-}
\ No newline at end of file
+
+    fun removeUnrankedNotifs(): Boolean =
+        featureFlags.isEnabled(Flags.REMOVE_UNRANKED_NOTIFICATIONS)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
index 38830c2..e345aab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotifCollection.java
@@ -601,6 +601,12 @@
         );
         mNotificationsWithoutRankings = currentEntriesWithoutRankings == null
                 ? Collections.emptySet() : currentEntriesWithoutRankings.keySet();
+        if (currentEntriesWithoutRankings != null && mNotifPipelineFlags.removeUnrankedNotifs()) {
+            for (NotificationEntry entry : currentEntriesWithoutRankings.values()) {
+                entry.mCancellationReason = REASON_UNKNOWN;
+                tryRemoveNotification(entry);
+            }
+        }
         mEventQueue.add(new RankingAppliedEvent());
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
index 702c6da..6441d2f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilder.java
@@ -986,7 +986,7 @@
         // Check for suppressed order changes
         if (!getStabilityManager().isEveryChangeAllowed()) {
             mForceReorderable = true;
-            boolean isSorted = isSorted(mNotifList, mTopLevelComparator);
+            boolean isSorted = isShadeSorted();
             mForceReorderable = false;
             if (!isSorted) {
                 getStabilityManager().onEntryReorderSuppressed();
@@ -995,9 +995,23 @@
         Trace.endSection();
     }
 
+    private boolean isShadeSorted() {
+        if (!isSorted(mNotifList, mTopLevelComparator)) {
+            return false;
+        }
+        for (ListEntry entry : mNotifList) {
+            if (entry instanceof GroupEntry) {
+                if (!isSorted(((GroupEntry) entry).getChildren(), mGroupChildrenComparator)) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
     /** Determine whether the items in the list are sorted according to the comparator */
     @VisibleForTesting
-    public static <T> boolean isSorted(List<T> items, Comparator<T> comparator) {
+    public static <T> boolean isSorted(List<T> items, Comparator<? super T> comparator) {
         if (items.size() <= 1) {
             return true;
         }
@@ -1205,7 +1219,7 @@
     };
 
 
-    private final Comparator<ListEntry> mGroupChildrenComparator = (o1, o2) -> {
+    private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
         int index1 = canReorder(o1) ? -1 : o1.getPreviousAttachState().getStableIndex();
         int index2 = canReorder(o2) ? -1 : o2.getPreviousAttachState().getStableIndex();
         int cmp = Integer.compare(index1, index2);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index fd307df..93b2e41 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -28,17 +28,13 @@
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE;
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE;
 
-import android.app.ActivityManager;
 import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.admin.DevicePolicyManager;
 import android.content.BroadcastReceiver;
-import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
-import android.content.ServiceConnection;
-import android.content.pm.ActivityInfo;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.ColorStateList;
@@ -46,13 +42,8 @@
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.Bundle;
-import android.os.IBinder;
-import android.os.Message;
-import android.os.Messenger;
 import android.os.RemoteException;
 import android.os.UserHandle;
-import android.provider.MediaStore;
-import android.service.media.CameraPrewarmService;
 import android.service.quickaccesswallet.GetWalletCardsError;
 import android.service.quickaccesswallet.GetWalletCardsResponse;
 import android.service.quickaccesswallet.QuickAccessWalletClient;
@@ -172,20 +163,6 @@
     private KeyguardAffordanceHelper mAffordanceHelper;
     private FalsingManager mFalsingManager;
     private boolean mUserSetupComplete;
-    private boolean mPrewarmBound;
-    private Messenger mPrewarmMessenger;
-    private final ServiceConnection mPrewarmConnection = new ServiceConnection() {
-
-        @Override
-        public void onServiceConnected(ComponentName name, IBinder service) {
-            mPrewarmMessenger = new Messenger(service);
-        }
-
-        @Override
-        public void onServiceDisconnected(ComponentName name) {
-            mPrewarmMessenger = null;
-        }
-    };
 
     private boolean mLeftIsVoiceAssist;
     private Drawable mLeftAssistIcon;
@@ -602,46 +579,6 @@
         }
     }
 
-    public void bindCameraPrewarmService() {
-        Intent intent = getCameraIntent();
-        ActivityInfo targetInfo = mActivityIntentHelper.getTargetActivityInfo(intent,
-                KeyguardUpdateMonitor.getCurrentUser(), true /* onlyDirectBootAware */);
-        if (targetInfo != null && targetInfo.metaData != null) {
-            String clazz = targetInfo.metaData.getString(
-                    MediaStore.META_DATA_STILL_IMAGE_CAMERA_PREWARM_SERVICE);
-            if (clazz != null) {
-                Intent serviceIntent = new Intent();
-                serviceIntent.setClassName(targetInfo.packageName, clazz);
-                serviceIntent.setAction(CameraPrewarmService.ACTION_PREWARM);
-                try {
-                    if (getContext().bindServiceAsUser(serviceIntent, mPrewarmConnection,
-                            Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
-                            new UserHandle(UserHandle.USER_CURRENT))) {
-                        mPrewarmBound = true;
-                    }
-                } catch (SecurityException e) {
-                    Log.w(TAG, "Unable to bind to prewarm service package=" + targetInfo.packageName
-                            + " class=" + clazz, e);
-                }
-            }
-        }
-    }
-
-    public void unbindCameraPrewarmService(boolean launched) {
-        if (mPrewarmBound) {
-            if (mPrewarmMessenger != null && launched) {
-                try {
-                    mPrewarmMessenger.send(Message.obtain(null /* handler */,
-                            CameraPrewarmService.MSG_CAMERA_FIRED));
-                } catch (RemoteException e) {
-                    Log.w(TAG, "Error sending camera fired message", e);
-                }
-            }
-            mContext.unbindService(mPrewarmConnection);
-            mPrewarmBound = false;
-        }
-    }
-
     public void launchCamera(String source) {
         final Intent intent = getCameraIntent();
         intent.putExtra(EXTRA_CAMERA_LAUNCH_SOURCE, source);
@@ -651,8 +588,6 @@
             AsyncTask.execute(new Runnable() {
                 @Override
                 public void run() {
-                    int result = ActivityManager.START_CANCELED;
-
                     // Normally an activity will set it's requested rotation
                     // animation on its window. However when launching an activity
                     // causes the orientation to change this is too late. In these cases
@@ -666,7 +601,7 @@
                     o.setRotationAnimationHint(
                             WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS);
                     try {
-                        result = ActivityTaskManager.getService().startActivityAsUser(
+                        ActivityTaskManager.getService().startActivityAsUser(
                                 null, getContext().getBasePackageName(),
                                 getContext().getAttributionTag(), intent,
                                 intent.resolveTypeIfNeeded(getContext().getContentResolver()),
@@ -675,25 +610,12 @@
                     } catch (RemoteException e) {
                         Log.w(TAG, "Unable to start camera activity", e);
                     }
-                    final boolean launched = isSuccessfulLaunch(result);
-                    post(new Runnable() {
-                        @Override
-                        public void run() {
-                            unbindCameraPrewarmService(launched);
-                        }
-                    });
                 }
             });
         } else {
             // We need to delay starting the activity because ResolverActivity finishes itself if
             // launched behind lockscreen.
-            mActivityStarter.startActivity(intent, false /* dismissShade */,
-                    new ActivityStarter.Callback() {
-                        @Override
-                        public void onActivityStarted(int resultCode) {
-                            unbindCameraPrewarmService(isSuccessfulLaunch(resultCode));
-                        }
-                    });
+            mActivityStarter.startActivity(intent, false /* dismissShade */);
         }
     }
 
@@ -705,12 +627,6 @@
         dozeTimeTick();
     }
 
-    private static boolean isSuccessfulLaunch(int result) {
-        return result == ActivityManager.START_SUCCESS
-                || result == ActivityManager.START_DELIVERED_TO_TOP
-                || result == ActivityManager.START_TASK_TO_FRONT;
-    }
-
     public void launchLeftAffordance() {
         if (mLeftIsVoiceAssist) {
             launchVoiceAssist();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
index 7077300..fbbb587 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java
@@ -4591,13 +4591,6 @@
         @Override
         public void onSwipingStarted(boolean rightIcon) {
             mFalsingCollector.onAffordanceSwipingStarted(rightIcon);
-            boolean
-                    camera =
-                    mView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? !rightIcon
-                            : rightIcon;
-            if (camera) {
-                mKeyguardBottomArea.bindCameraPrewarmService();
-            }
             mView.requestDisallowInterceptTouchEvent(true);
             mOnlyAffordanceInThisMotion = true;
             mQsTracking = false;
@@ -4606,7 +4599,6 @@
         @Override
         public void onSwipingAborted() {
             mFalsingCollector.onAffordanceSwipingAborted();
-            mKeyguardBottomArea.unbindCameraPrewarmService(false /* launched */);
         }
 
         @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
index 36a0456..26bc3e3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/SystemUIDialog.java
@@ -93,6 +93,17 @@
     }
 
     public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock) {
+        // TODO(b/219008720): Remove those calls to Dependency.get by introducing a
+        // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set
+        // the content and attach listeners.
+        this(context, theme, dismissOnDeviceLock, Dependency.get(SystemUIDialogManager.class),
+                Dependency.get(SysUiState.class), Dependency.get(BroadcastDispatcher.class),
+                Dependency.get(DialogLaunchAnimator.class));
+    }
+
+    public SystemUIDialog(Context context, int theme, boolean dismissOnDeviceLock,
+            SystemUIDialogManager dialogManager, SysUiState sysUiState,
+            BroadcastDispatcher broadcastDispatcher, DialogLaunchAnimator dialogLaunchAnimator) {
         super(context, theme);
         mContext = context;
 
@@ -101,13 +112,10 @@
         attrs.setTitle(getClass().getSimpleName());
         getWindow().setAttributes(attrs);
 
-        mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this) : null;
-
-        // TODO(b/219008720): Remove those calls to Dependency.get by introducing a
-        // SystemUIDialogFactory and make all other dialogs create a SystemUIDialog to which we set
-        // the content and attach listeners.
-        mDialogManager = Dependency.get(SystemUIDialogManager.class);
-        mSysUiState = Dependency.get(SysUiState.class);
+        mDismissReceiver = dismissOnDeviceLock ? new DismissReceiver(this, broadcastDispatcher,
+                dialogLaunchAnimator) : null;
+        mDialogManager = dialogManager;
+        mSysUiState = sysUiState;
     }
 
     @Override
@@ -326,7 +334,10 @@
      * @param dismissAction An action to run when the dialog is dismissed.
      */
     public static void registerDismissListener(Dialog dialog, @Nullable Runnable dismissAction) {
-        DismissReceiver dismissReceiver = new DismissReceiver(dialog);
+        // TODO(b/219008720): Remove those calls to Dependency.get.
+        DismissReceiver dismissReceiver = new DismissReceiver(dialog,
+                Dependency.get(BroadcastDispatcher.class),
+                Dependency.get(DialogLaunchAnimator.class));
         dialog.setOnDismissListener(d -> {
             dismissReceiver.unregister();
             if (dismissAction != null) dismissAction.run();
@@ -408,11 +419,11 @@
         private final BroadcastDispatcher mBroadcastDispatcher;
         private final DialogLaunchAnimator mDialogLaunchAnimator;
 
-        DismissReceiver(Dialog dialog) {
+        DismissReceiver(Dialog dialog, BroadcastDispatcher broadcastDispatcher,
+                DialogLaunchAnimator dialogLaunchAnimator) {
             mDialog = dialog;
-            // TODO(b/219008720): Remove those calls to Dependency.get.
-            mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class);
-            mDialogLaunchAnimator = Dependency.get(DialogLaunchAnimator.class);
+            mBroadcastDispatcher = broadcastDispatcher;
+            mDialogLaunchAnimator = dialogLaunchAnimator;
         }
 
         void register() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
index 489c8c8..bf237ab 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSContainerImplTest.kt
@@ -57,6 +57,7 @@
 
     @Test
     fun testContainerBottomPadding() {
+        val originalPadding = qsPanelContainer.paddingBottom
         qsContainer.updateResources(
             qsPanelController,
             quickStatusBarHeaderController
@@ -66,7 +67,7 @@
                 anyInt(),
                 anyInt(),
                 anyInt(),
-                eq(mContext.resources.getDimensionPixelSize(R.dimen.footer_actions_height))
+                eq(originalPadding)
             )
     }
 }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index 60cfd72..b98be75 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -150,6 +150,14 @@
         assertThat(footer.isVisibleToUser).isTrue()
     }
 
+    @Test
+    fun testBottomPadding() {
+        val padding = 10
+        context.orCreateTestableResources.addOverride(R.dimen.qs_panel_padding_bottom, padding)
+        qsPanel.updatePadding()
+        assertThat(qsPanel.paddingBottom).isEqualTo(padding)
+    }
+
     private infix fun View.isLeftOf(other: View): Boolean {
         val rect = Rect()
         getBoundsOnScreen(rect)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java
index 4507366..ee7d558 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NoManSimulator.java
@@ -85,6 +85,11 @@
         mRankings.put(key, ranking);
     }
 
+    /** This is for testing error cases: b/216384850 */
+    public Ranking removeRankingWithoutEvent(String key) {
+        return mRankings.remove(key);
+    }
+
     private RankingMap buildRankingMap() {
         return new RankingMap(mRankings.values().toArray(new Ranking[0]));
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
index 958d542..f286349 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotifCollectionTest.java
@@ -1492,6 +1492,80 @@
     }
 
     @Test
+    public void testMissingRankingWhenRemovalFeatureIsDisabled() {
+        // GIVEN a pipeline with one two notifications
+        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(false);
+        String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
+        String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
+        NotificationEntry entry1 = mCollectionListener.getEntry(key1);
+        NotificationEntry entry2 = mCollectionListener.getEntry(key2);
+        clearInvocations(mCollectionListener);
+
+        // GIVEN the message for removing key1 gets does not reach NotifCollection
+        Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1);
+        // WHEN the message for removing key2 arrives
+        mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL);
+
+        // THEN only entry2 gets removed
+        verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL));
+        verify(mCollectionListener).onEntryCleanUp(eq(entry2));
+        verify(mCollectionListener).onRankingApplied();
+        verifyNoMoreInteractions(mCollectionListener);
+        verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
+        verify(mLogger, never()).logRecoveredRankings(any());
+        clearInvocations(mCollectionListener, mLogger);
+
+        // WHEN a ranking update includes key1 again
+        mNoMan.setRanking(key1, ranking1);
+        mNoMan.issueRankingUpdate();
+
+        // VERIFY that we do nothing but log the 'recovery'
+        verify(mCollectionListener).onRankingUpdate(any());
+        verify(mCollectionListener).onRankingApplied();
+        verifyNoMoreInteractions(mCollectionListener);
+        verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
+        verify(mLogger).logRecoveredRankings(eq(List.of(key1)));
+    }
+
+    @Test
+    public void testMissingRankingWhenRemovalFeatureIsEnabled() {
+        // GIVEN a pipeline with one two notifications
+        when(mNotifPipelineFlags.removeUnrankedNotifs()).thenReturn(true);
+        String key1 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 1, "myTag")).key;
+        String key2 = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 2, "myTag")).key;
+        NotificationEntry entry1 = mCollectionListener.getEntry(key1);
+        NotificationEntry entry2 = mCollectionListener.getEntry(key2);
+        clearInvocations(mCollectionListener);
+
+        // GIVEN the message for removing key1 gets does not reach NotifCollection
+        Ranking ranking1 = mNoMan.removeRankingWithoutEvent(key1);
+        // WHEN the message for removing key2 arrives
+        mNoMan.retractNotif(entry2.getSbn(), REASON_APP_CANCEL);
+
+        // THEN both entry1 and entry2 get removed
+        verify(mCollectionListener).onEntryRemoved(eq(entry2), eq(REASON_APP_CANCEL));
+        verify(mCollectionListener).onEntryRemoved(eq(entry1), eq(REASON_UNKNOWN));
+        verify(mCollectionListener).onEntryCleanUp(eq(entry2));
+        verify(mCollectionListener).onEntryCleanUp(eq(entry1));
+        verify(mCollectionListener).onRankingApplied();
+        verifyNoMoreInteractions(mCollectionListener);
+        verify(mLogger).logMissingRankings(eq(List.of(entry1)), eq(1), any());
+        verify(mLogger, never()).logRecoveredRankings(any());
+        clearInvocations(mCollectionListener, mLogger);
+
+        // WHEN a ranking update includes key1 again
+        mNoMan.setRanking(key1, ranking1);
+        mNoMan.issueRankingUpdate();
+
+        // VERIFY that we do nothing but log the 'recovery'
+        verify(mCollectionListener).onRankingUpdate(any());
+        verify(mCollectionListener).onRankingApplied();
+        verifyNoMoreInteractions(mCollectionListener);
+        verify(mLogger, never()).logMissingRankings(any(), anyInt(), any());
+        verify(mLogger).logRecoveredRankings(eq(List.of(key1)));
+    }
+
+    @Test
     public void testRegisterFutureDismissal() throws RemoteException {
         // GIVEN a pipeline with one notification
         NotifEvent notifEvent = mNoMan.postNotif(buildNotif(TEST_PACKAGE, 47, "myTag"));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
index 4e7e79f..9546058 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/ShadeListBuilderTest.java
@@ -53,6 +53,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.statusbar.NotificationInteractionTracker;
+import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.collection.ShadeListBuilder.OnRenderListListener;
 import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection;
@@ -1797,6 +1798,7 @@
 
     @Test
     public void testStableMultipleSectionOrdering() {
+        // WHEN the list is originally built with reordering disabled
         mListBuilder.setSectioners(asList(
                 new PackageSectioner(PACKAGE_1), new PackageSectioner(PACKAGE_2)));
         mStabilityManager.setAllowEntryReordering(false);
@@ -1807,12 +1809,94 @@
         addNotif(3, PACKAGE_1).setRank(3);
         dispatchBuild();
 
+        // VERIFY the order and that entry reordering has not been suppressed
         verifyBuiltList(
                 notif(0),
                 notif(1),
                 notif(3),
                 notif(2)
         );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+
+        // WHEN the ranks change
+        setNewRank(notif(0).entry, 4);
+        dispatchBuild();
+
+        // VERIFY the order does not change that entry reordering has been suppressed
+        verifyBuiltList(
+                notif(0),
+                notif(1),
+                notif(3),
+                notif(2)
+        );
+        verify(mStabilityManager).onEntryReorderSuppressed();
+
+        // WHEN reordering is now allowed again
+        mStabilityManager.setAllowEntryReordering(true);
+        dispatchBuild();
+
+        // VERIFY that list order changes
+        verifyBuiltList(
+                notif(1),
+                notif(3),
+                notif(0),
+                notif(2)
+        );
+    }
+
+    @Test
+    public void testStableChildOrdering() {
+        // WHEN the list is originally built with reordering disabled
+        mStabilityManager.setAllowEntryReordering(false);
+        addGroupSummary(0, PACKAGE_1, GROUP_1).setRank(0);
+        addGroupChild(1, PACKAGE_1, GROUP_1).setRank(1);
+        addGroupChild(2, PACKAGE_1, GROUP_1).setRank(2);
+        addGroupChild(3, PACKAGE_1, GROUP_1).setRank(3);
+        dispatchBuild();
+
+        // VERIFY the order and that entry reordering has not been suppressed
+        verifyBuiltList(
+                group(
+                        summary(0),
+                        child(1),
+                        child(2),
+                        child(3)
+                )
+        );
+        verify(mStabilityManager, never()).onEntryReorderSuppressed();
+
+        // WHEN the ranks change
+        setNewRank(notif(2).entry, 5);
+        dispatchBuild();
+
+        // VERIFY the order does not change that entry reordering has been suppressed
+        verifyBuiltList(
+                group(
+                        summary(0),
+                        child(1),
+                        child(2),
+                        child(3)
+                )
+        );
+        verify(mStabilityManager).onEntryReorderSuppressed();
+
+        // WHEN reordering is now allowed again
+        mStabilityManager.setAllowEntryReordering(true);
+        dispatchBuild();
+
+        // VERIFY that list order changes
+        verifyBuiltList(
+                group(
+                        summary(0),
+                        child(1),
+                        child(3),
+                        child(2)
+                )
+        );
+    }
+
+    private static void setNewRank(NotificationEntry entry, int rank) {
+        entry.setRanking(new RankingBuilder(entry.getRanking()).setRank(rank).build());
     }
 
     @Test
diff --git a/services/core/java/com/android/server/display/BrightnessThrottler.java b/services/core/java/com/android/server/display/BrightnessThrottler.java
index 767b2d1..eccee52 100644
--- a/services/core/java/com/android/server/display/BrightnessThrottler.java
+++ b/services/core/java/com/android/server/display/BrightnessThrottler.java
@@ -16,21 +16,31 @@
 
 package com.android.server.display;
 
+import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.display.BrightnessInfo;
+import android.hardware.display.DisplayManager;
 import android.os.Handler;
+import android.os.HandlerExecutor;
 import android.os.IThermalEventListener;
 import android.os.IThermalService;
 import android.os.PowerManager;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.os.Temperature;
+import android.provider.DeviceConfig;
+import android.provider.DeviceConfigInterface;
 import android.util.Slog;
 
-import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel;
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData;
+import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel;
 
 import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.Executor;
 
 /**
  * This class monitors various conditions, such as skin temperature throttling status, and limits
@@ -44,28 +54,54 @@
 
     private final Injector mInjector;
     private final Handler mHandler;
-    private BrightnessThrottlingData mThrottlingData;
+    // We need a separate handler for unit testing. These two handlers are the same throughout the
+    // non-test code.
+    private final Handler mDeviceConfigHandler;
     private final Runnable mThrottlingChangeCallback;
     private final SkinThermalStatusObserver mSkinThermalStatusObserver;
+    private final DeviceConfigListener mDeviceConfigListener;
+    private final DeviceConfigInterface mDeviceConfig;
+
     private int mThrottlingStatus;
+    private BrightnessThrottlingData mThrottlingData;
+    private BrightnessThrottlingData mDdcThrottlingData;
     private float mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
     private @BrightnessInfo.BrightnessMaxReason int mBrightnessMaxReason =
         BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
+    private String mUniqueDisplayId;
+
+    // The most recent string that has been set from DeviceConfig
+    private String mBrightnessThrottlingDataString;
+
+    // This is a collection of brightness throttling data that has been written as overrides from
+    // the DeviceConfig. This will always take priority over the display device config data.
+    private HashMap<String, BrightnessThrottlingData> mBrightnessThrottlingDataOverride =
+            new HashMap<>(1);
 
     BrightnessThrottler(Handler handler, BrightnessThrottlingData throttlingData,
-            Runnable throttlingChangeCallback) {
-        this(new Injector(), handler, throttlingData, throttlingChangeCallback);
+            Runnable throttlingChangeCallback, String uniqueDisplayId) {
+        this(new Injector(), handler, handler, throttlingData, throttlingChangeCallback,
+                uniqueDisplayId);
     }
 
-    BrightnessThrottler(Injector injector, Handler handler, BrightnessThrottlingData throttlingData,
-            Runnable throttlingChangeCallback) {
+    @VisibleForTesting
+    BrightnessThrottler(Injector injector, Handler handler, Handler deviceConfigHandler,
+            BrightnessThrottlingData throttlingData, Runnable throttlingChangeCallback,
+            String uniqueDisplayId) {
         mInjector = injector;
+
         mHandler = handler;
+        mDeviceConfigHandler = deviceConfigHandler;
         mThrottlingData = throttlingData;
+        mDdcThrottlingData = throttlingData;
         mThrottlingChangeCallback = throttlingChangeCallback;
         mSkinThermalStatusObserver = new SkinThermalStatusObserver(mInjector, mHandler);
 
-        resetThrottlingData(mThrottlingData);
+        mUniqueDisplayId = uniqueDisplayId;
+        mDeviceConfig = injector.getDeviceConfig();
+        mDeviceConfigListener = new DeviceConfigListener();
+
+        resetThrottlingData(mThrottlingData, mUniqueDisplayId);
     }
 
     boolean deviceSupportsThrottling() {
@@ -86,7 +122,7 @@
 
     void stop() {
         mSkinThermalStatusObserver.stopObserving();
-
+        mDeviceConfig.removeOnPropertiesChangedListener(mDeviceConfigListener);
         // We're asked to stop throttling, so reset brightness restrictions.
         mBrightnessCap = PowerManager.BRIGHTNESS_MAX;
         mBrightnessMaxReason = BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE;
@@ -97,9 +133,19 @@
         mThrottlingStatus = THROTTLING_INVALID;
     }
 
-    void resetThrottlingData(BrightnessThrottlingData throttlingData) {
+    private void resetThrottlingData() {
+        resetThrottlingData(mDdcThrottlingData, mUniqueDisplayId);
+    }
+
+    void resetThrottlingData(BrightnessThrottlingData throttlingData, String displayId) {
         stop();
-        mThrottlingData = throttlingData;
+
+        mUniqueDisplayId = displayId;
+        mDdcThrottlingData = throttlingData;
+        mDeviceConfigListener.startListening();
+        reloadBrightnessThrottlingDataOverride();
+        mThrottlingData = mBrightnessThrottlingDataOverride.getOrDefault(mUniqueDisplayId,
+                throttlingData);
 
         if (deviceSupportsThrottling()) {
             mSkinThermalStatusObserver.startObserving();
@@ -173,14 +219,148 @@
     private void dumpLocal(PrintWriter pw) {
         pw.println("BrightnessThrottler:");
         pw.println("  mThrottlingData=" + mThrottlingData);
+        pw.println("  mDdcThrottlingData=" + mDdcThrottlingData);
+        pw.println("  mUniqueDisplayId=" + mUniqueDisplayId);
         pw.println("  mThrottlingStatus=" + mThrottlingStatus);
         pw.println("  mBrightnessCap=" + mBrightnessCap);
         pw.println("  mBrightnessMaxReason=" +
             BrightnessInfo.briMaxReasonToString(mBrightnessMaxReason));
+        pw.println("  mBrightnessThrottlingDataOverride=" + mBrightnessThrottlingDataOverride);
+        pw.println("  mBrightnessThrottlingDataString=" + mBrightnessThrottlingDataString);
 
         mSkinThermalStatusObserver.dump(pw);
     }
 
+    private String getBrightnessThrottlingDataString() {
+        return mDeviceConfig.getString(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA,
+                /* defaultValue= */ null);
+    }
+
+    private boolean parseAndSaveData(@NonNull String strArray,
+            @NonNull HashMap<String, BrightnessThrottlingData> tempBrightnessThrottlingData) {
+        boolean validConfig = true;
+        String[] items = strArray.split(",");
+        int i = 0;
+
+        try {
+            String uniqueDisplayId = items[i++];
+
+            // number of throttling points
+            int noOfThrottlingPoints = Integer.parseInt(items[i++]);
+            List<ThrottlingLevel> throttlingLevels = new ArrayList<>(noOfThrottlingPoints);
+
+            // throttling level and point
+            for (int j = 0; j < noOfThrottlingPoints; j++) {
+                String severity = items[i++];
+                int status = parseThermalStatus(severity);
+
+                float brightnessPoint = parseBrightness(items[i++]);
+
+                throttlingLevels.add(new ThrottlingLevel(status, brightnessPoint));
+            }
+            BrightnessThrottlingData toSave =
+                    DisplayDeviceConfig.BrightnessThrottlingData.create(throttlingLevels);
+            tempBrightnessThrottlingData.put(uniqueDisplayId, toSave);
+        } catch (NumberFormatException | IndexOutOfBoundsException
+                | UnknownThermalStatusException e) {
+            validConfig = false;
+            Slog.e(TAG, "Throttling data is invalid array: '" + strArray + "'", e);
+        }
+
+        if (i != items.length) {
+            validConfig = false;
+        }
+
+        return validConfig;
+    }
+
+    public void reloadBrightnessThrottlingDataOverride() {
+        HashMap<String, BrightnessThrottlingData> tempBrightnessThrottlingData =
+                new HashMap<>(1);
+        mBrightnessThrottlingDataString = getBrightnessThrottlingDataString();
+        boolean validConfig = true;
+        mBrightnessThrottlingDataOverride.clear();
+        if (mBrightnessThrottlingDataString != null) {
+            String[] throttlingDataSplits = mBrightnessThrottlingDataString.split(";");
+            for (String s : throttlingDataSplits) {
+                if (!parseAndSaveData(s, tempBrightnessThrottlingData)) {
+                    validConfig = false;
+                    break;
+                }
+            }
+
+            if (validConfig) {
+                mBrightnessThrottlingDataOverride.putAll(tempBrightnessThrottlingData);
+                tempBrightnessThrottlingData.clear();
+            }
+
+        } else {
+            Slog.w(TAG, "DeviceConfig BrightnessThrottlingData is null");
+        }
+    }
+
+    /**
+     * Listens to config data change and updates the brightness throttling data using
+     * DisplayManager#KEY_BRIGHTNESS_THROTTLING_DATA.
+     * The format should be a string similar to: "local:4619827677550801152,2,moderate,0.5,severe,
+     * 0.379518072;local:4619827677550801151,1,moderate,0.75"
+     * In this order:
+     * <displayId>,<no of throttling levels>,[<severity as string>,<brightness cap>]
+     * Where the latter part is repeated for each throttling level, and the entirety is repeated
+     * for each display, separated by a semicolon.
+     */
+    public class DeviceConfigListener implements DeviceConfig.OnPropertiesChangedListener {
+        public Executor mExecutor = new HandlerExecutor(mDeviceConfigHandler);
+
+        public void startListening() {
+            mDeviceConfig.addOnPropertiesChangedListener(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                    mExecutor, this);
+        }
+
+        @Override
+        public void onPropertiesChanged(DeviceConfig.Properties properties) {
+            reloadBrightnessThrottlingDataOverride();
+            resetThrottlingData();
+        }
+    }
+
+    private float parseBrightness(String intVal) throws NumberFormatException {
+        float value = Float.parseFloat(intVal);
+        if (value < PowerManager.BRIGHTNESS_MIN || value > PowerManager.BRIGHTNESS_MAX) {
+            throw new NumberFormatException("Brightness constraint value out of bounds.");
+        }
+        return value;
+    }
+
+    @PowerManager.ThermalStatus private int parseThermalStatus(@NonNull String value)
+            throws UnknownThermalStatusException {
+        switch (value) {
+            case "none":
+                return PowerManager.THERMAL_STATUS_NONE;
+            case "light":
+                return PowerManager.THERMAL_STATUS_LIGHT;
+            case "moderate":
+                return PowerManager.THERMAL_STATUS_MODERATE;
+            case "severe":
+                return PowerManager.THERMAL_STATUS_SEVERE;
+            case "critical":
+                return PowerManager.THERMAL_STATUS_CRITICAL;
+            case "emergency":
+                return PowerManager.THERMAL_STATUS_EMERGENCY;
+            case "shutdown":
+                return PowerManager.THERMAL_STATUS_SHUTDOWN;
+            default:
+                throw new UnknownThermalStatusException("Invalid Thermal Status: " + value);
+        }
+    }
+
+    private static class UnknownThermalStatusException extends Exception {
+        UnknownThermalStatusException(String message) {
+            super(message);
+        }
+    }
+
     private final class SkinThermalStatusObserver extends IThermalEventListener.Stub {
         private final Injector mInjector;
         private final Handler mHandler;
@@ -258,5 +438,10 @@
             return IThermalService.Stub.asInterface(
                     ServiceManager.getService(Context.THERMAL_SERVICE));
         }
+
+        @NonNull
+        public DeviceConfigInterface getDeviceConfig() {
+            return DeviceConfigInterface.REAL;
+        }
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index a25ac21..2322280d 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -279,10 +279,14 @@
     private HighBrightnessModeData mHbmData;
     private DensityMapping mDensityMapping;
     private String mLoadedFrom = null;
-
-    private BrightnessThrottlingData mBrightnessThrottlingData;
     private Spline mSdrToHdrRatioSpline;
 
+    // Brightness Throttling data may be updated via the DeviceConfig. Here we store the original
+    // data, which comes from the ddc, and the current one, which may be the DeviceConfig
+    // overwritten value.
+    private BrightnessThrottlingData mBrightnessThrottlingData;
+    private BrightnessThrottlingData mOriginalBrightnessThrottlingData;
+
     private DisplayDeviceConfig(Context context) {
         mContext = context;
     }
@@ -422,6 +426,10 @@
         return config;
     }
 
+    void setBrightnessThrottlingData(BrightnessThrottlingData brightnessThrottlingData) {
+        mBrightnessThrottlingData = brightnessThrottlingData;
+    }
+
     /**
      * Return the brightness mapping nits array.
      *
@@ -637,6 +645,7 @@
                 + ", mHbmData=" + mHbmData
                 + ", mSdrToHdrRatioSpline=" + mSdrToHdrRatioSpline
                 + ", mBrightnessThrottlingData=" + mBrightnessThrottlingData
+                + ", mOriginalBrightnessThrottlingData=" + mOriginalBrightnessThrottlingData
                 + ", mBrightnessRampFastDecrease=" + mBrightnessRampFastDecrease
                 + ", mBrightnessRampFastIncrease=" + mBrightnessRampFastIncrease
                 + ", mBrightnessRampSlowDecrease=" + mBrightnessRampSlowDecrease
@@ -932,6 +941,7 @@
 
         if (!badConfig) {
             mBrightnessThrottlingData = BrightnessThrottlingData.create(throttlingLevels);
+            mOriginalBrightnessThrottlingData = mBrightnessThrottlingData;
         }
     }
 
@@ -1407,7 +1417,9 @@
     /**
      * Container for brightness throttling data.
      */
-    static class BrightnessThrottlingData {
+    public static class BrightnessThrottlingData {
+        public List<ThrottlingLevel> throttlingLevels;
+
         static class ThrottlingLevel {
             public @PowerManager.ThermalStatus int thermalStatus;
             public float brightness;
@@ -1421,9 +1433,25 @@
             public String toString() {
                 return "[" + thermalStatus + "," + brightness + "]";
             }
-        }
 
-        public List<ThrottlingLevel> throttlingLevels;
+            @Override
+            public boolean equals(Object obj) {
+                if (!(obj instanceof ThrottlingLevel)) {
+                    return false;
+                }
+                ThrottlingLevel otherThrottlingLevel = (ThrottlingLevel) obj;
+
+                return otherThrottlingLevel.thermalStatus == this.thermalStatus
+                        && otherThrottlingLevel.brightness == this.brightness;
+            }
+            @Override
+            public int hashCode() {
+                int result = 1;
+                result = 31 * result + thermalStatus;
+                result = 31 * result + Float.hashCode(brightness);
+                return result;
+            }
+        }
 
         static public BrightnessThrottlingData create(List<ThrottlingLevel> throttlingLevels)
         {
@@ -1482,12 +1510,30 @@
                 + "} ";
         }
 
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+
+            if (!(obj instanceof BrightnessThrottlingData)) {
+                return false;
+            }
+
+            BrightnessThrottlingData otherBrightnessThrottlingData = (BrightnessThrottlingData) obj;
+            return throttlingLevels.equals(otherBrightnessThrottlingData.throttlingLevels);
+        }
+
+        @Override
+        public int hashCode() {
+            return throttlingLevels.hashCode();
+        }
+
         private BrightnessThrottlingData(List<ThrottlingLevel> inLevels) {
             throttlingLevels = new ArrayList<>(inLevels.size());
             for (ThrottlingLevel level : inLevels) {
                 throttlingLevels.add(new ThrottlingLevel(level.thermalStatus, level.brightness));
             }
         }
-
     }
 }
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index d05a902..95c8fef 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -461,6 +461,18 @@
 
     private boolean mIsRbcActive;
 
+    // Whether there's a callback to tell listeners the display has changed scheduled to run. When
+    // true it implies a wakelock is being held to guarantee the update happens before we collapse
+    // into suspend and so needs to be cleaned up if the thread is exiting.
+    // Should only be accessed on the Handler thread.
+    private boolean mOnStateChangedPending;
+
+    // Count of proximity messages currently on this DPC's Handler. Used to keep track of how many
+    // suspend blocker acquisitions are pending when shutting down this DPC.
+    // Should only be accessed on the Handler thread.
+    private int mOnProximityPositiveMessages;
+    private int mOnProximityNegativeMessages;
+
     // Animators.
     private ObjectAnimator mColorFadeOnAnimator;
     private ObjectAnimator mColorFadeOffAnimator;
@@ -861,7 +873,7 @@
                     }
                 });
         mBrightnessThrottler.resetThrottlingData(
-                mDisplayDeviceConfig.getBrightnessThrottlingData());
+                mDisplayDeviceConfig.getBrightnessThrottlingData(), mUniqueDisplayId);
     }
 
     private void sendUpdatePowerState() {
@@ -1091,10 +1103,24 @@
         mHbmController.stop();
         mBrightnessThrottler.stop();
         mHandler.removeCallbacksAndMessages(null);
+
+        // Release any outstanding wakelocks we're still holding because of pending messages.
         if (mUnfinishedBusiness) {
             mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdUnfinishedBusiness);
             mUnfinishedBusiness = false;
         }
+        if (mOnStateChangedPending) {
+            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
+            mOnStateChangedPending = false;
+        }
+        for (int i = 0; i < mOnProximityPositiveMessages; i++) {
+            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
+        }
+        mOnProximityPositiveMessages = 0;
+        for (int i = 0; i < mOnProximityNegativeMessages; i++) {
+            mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
+        }
+        mOnProximityNegativeMessages = 0;
 
         final float brightness = mPowerState != null
             ? mPowerState.getScreenBrightness()
@@ -1816,7 +1842,7 @@
                 () -> {
                     sendUpdatePowerStateLocked();
                     postBrightnessChangeRunnable();
-                });
+                }, mUniqueDisplayId);
     }
 
     private void blockScreenOn() {
@@ -2248,8 +2274,11 @@
     }
 
     private void sendOnStateChangedWithWakelock() {
-        mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged);
-        mHandler.post(mOnStateChangedRunnable);
+        if (!mOnStateChangedPending) {
+            mOnStateChangedPending = true;
+            mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdOnStateChanged);
+            mHandler.post(mOnStateChangedRunnable);
+        }
     }
 
     private void logDisplayPolicyChanged(int newPolicy) {
@@ -2408,6 +2437,7 @@
     private final Runnable mOnStateChangedRunnable = new Runnable() {
         @Override
         public void run() {
+            mOnStateChangedPending = false;
             mCallbacks.onStateChanged();
             mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdOnStateChanged);
         }
@@ -2416,17 +2446,20 @@
     private void sendOnProximityPositiveWithWakelock() {
         mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxPositive);
         mHandler.post(mOnProximityPositiveRunnable);
+        mOnProximityPositiveMessages++;
     }
 
     private final Runnable mOnProximityPositiveRunnable = new Runnable() {
         @Override
         public void run() {
+            mOnProximityPositiveMessages--;
             mCallbacks.onProximityPositive();
             mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxPositive);
         }
     };
 
     private void sendOnProximityNegativeWithWakelock() {
+        mOnProximityNegativeMessages++;
         mCallbacks.acquireSuspendBlocker(mSuspendBlockerIdProxNegative);
         mHandler.post(mOnProximityNegativeRunnable);
     }
@@ -2434,6 +2467,7 @@
     private final Runnable mOnProximityNegativeRunnable = new Runnable() {
         @Override
         public void run() {
+            mOnProximityNegativeMessages--;
             mCallbacks.onProximityNegative();
             mCallbacks.releaseSuspendBlocker(mSuspendBlockerIdProxNegative);
         }
@@ -2533,6 +2567,9 @@
         pw.println("  mReportedToPolicy="
                 + reportedToPolicyToString(mReportedScreenStateToPolicy));
         pw.println("  mIsRbcActive=" + mIsRbcActive);
+        pw.println("  mOnStateChangePending=" + mOnStateChangedPending);
+        pw.println("  mOnProximityPositiveMessages=" + mOnProximityPositiveMessages);
+        pw.println("  mOnProximityNegativeMessages=" + mOnProximityNegativeMessages);
 
         if (mScreenBrightnessRampAnimator != null) {
             pw.println("  mScreenBrightnessRampAnimator.isAnimating()="
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index ad3b8ee..96d9d66 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -2941,9 +2941,10 @@
             // Set some sort of reasonable bounds on the size of the display that we will try
             // to emulate.
             final int minSize = 200;
-            final int maxScale = 2;
-            width = Math.min(Math.max(width, minSize), mInitialDisplayWidth * maxScale);
-            height = Math.min(Math.max(height, minSize), mInitialDisplayHeight * maxScale);
+            final int maxScale = 3;
+            final int maxSize = Math.max(mInitialDisplayWidth, mInitialDisplayHeight) * maxScale;
+            width = Math.min(Math.max(width, minSize), maxSize);
+            height = Math.min(Math.max(height, minSize), maxSize);
         }
 
         Slog.i(TAG_WM, "Using new display size: " + width + "x" + height);
diff --git a/services/core/java/com/android/server/wm/DisplayRotation.java b/services/core/java/com/android/server/wm/DisplayRotation.java
index d2c71f5..b9d8319 100644
--- a/services/core/java/com/android/server/wm/DisplayRotation.java
+++ b/services/core/java/com/android/server/wm/DisplayRotation.java
@@ -620,7 +620,8 @@
         // We only enable seamless rotation if the top window has requested it and is in the
         // fullscreen opaque state. Seamless rotation requires freezing various Surface states and
         // won't work well with animations, so we disable it in the animation case for now.
-        if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.isAnimatingLw()) {
+        if (w.getAttrs().rotationAnimation != ROTATION_ANIMATION_SEAMLESS || w.inMultiWindowMode()
+                || w.isAnimatingLw()) {
             return false;
         }
 
diff --git a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java b/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java
index 0ed90d2..6a6cd6c 100644
--- a/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/BrightnessThrottlerTest.java
@@ -16,13 +16,11 @@
 
 package com.android.server.display;
 
-import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
-import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -34,17 +32,18 @@
 import android.os.IThermalService;
 import android.os.Message;
 import android.os.PowerManager;
-import android.os.Temperature.ThrottlingStatus;
 import android.os.Temperature;
+import android.os.Temperature.ThrottlingStatus;
 import android.os.test.TestLooper;
 import android.platform.test.annotations.Presubmit;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.os.BackgroundThread;
 import com.android.server.display.BrightnessThrottler.Injector;
-import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel;
 import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData;
+import com.android.server.display.DisplayDeviceConfig.BrightnessThrottlingData.ThrottlingLevel;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -55,7 +54,6 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.List;
 
 @SmallTest
@@ -70,6 +68,8 @@
     @Mock IThermalService mThermalServiceMock;
     @Mock Injector mInjectorMock;
 
+    DisplayModeDirectorTest.FakeDeviceConfig mDeviceConfigFake;
+
     @Captor ArgumentCaptor<IThermalEventListener> mThermalEventListenerCaptor;
 
     @Before
@@ -83,6 +83,8 @@
                 return true;
             }
         });
+        mDeviceConfigFake = new DisplayModeDirectorTest.FakeDeviceConfig();
+        when(mInjectorMock.getDeviceConfig()).thenReturn(mDeviceConfigFake);
 
     }
 
@@ -292,6 +294,170 @@
         assertEquals(BrightnessInfo.BRIGHTNESS_MAX_REASON_NONE, throttler.getBrightnessMaxReason());
     }
 
+    @Test public void testUpdateThrottlingData() throws Exception {
+        // Initialise brightness throttling levels
+        // Ensure that they are overridden by setting the data through device config.
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,0.4");
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.4f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // Update thresholds
+        // This data is equivalent to the string "123,1,critical,0.8", passed below
+        final ThrottlingLevel newLevel = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.8f);
+        // Set new (valid) data from device config
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,0.8");
+
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(newLevel.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(newLevel.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(newLevel.brightness, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+    }
+
+    @Test public void testInvalidThrottlingStrings() throws Exception {
+        // Initialise brightness throttling levels
+        // Ensure that they are not overridden by invalid data through device config.
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels);
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // None of these are valid so shouldn't override the original data
+        mDeviceConfigFake.setBrightnessThrottlingData("321,1,critical,0.4");  // Not the current id
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,0,critical,0.4");  // Incorrect number
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,2,critical,0.4");  // Incorrect number
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,invalid,0.4");   // Invalid level
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,none"); // Invalid brightness
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("123,1,critical,-3");   // Invalid brightness
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("invalid string");      // Invalid format
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+        mDeviceConfigFake.setBrightnessThrottlingData("");                    // Invalid format
+        testThrottling(throttler, listener, PowerManager.BRIGHTNESS_MAX, 0.25f);
+    }
+
+    private void testThrottling(BrightnessThrottler throttler, IThermalEventListener listener,
+            float tooLowCap, float tooHighCap) throws Exception {
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                tooHighCap);
+
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(tooLowCap, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(level.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(tooHighCap, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+    }
+
+    @Test public void testMultipleConfigPoints() throws Exception {
+        // Initialise brightness throttling levels
+        final ThrottlingLevel level = new ThrottlingLevel(PowerManager.THERMAL_STATUS_CRITICAL,
+                0.25f);
+        List<ThrottlingLevel> levels = new ArrayList<>();
+        levels.add(level);
+        final BrightnessThrottlingData data = BrightnessThrottlingData.create(levels);
+
+        // These are identical to the string set below
+        final ThrottlingLevel levelSevere = new ThrottlingLevel(PowerManager.THERMAL_STATUS_SEVERE,
+                0.9f);
+        final ThrottlingLevel levelCritical = new ThrottlingLevel(
+                PowerManager.THERMAL_STATUS_CRITICAL, 0.5f);
+        final ThrottlingLevel levelEmergency = new ThrottlingLevel(
+                PowerManager.THERMAL_STATUS_EMERGENCY, 0.1f);
+
+        mDeviceConfigFake.setBrightnessThrottlingData(
+                "123,3,severe,0.9,critical,0.5,emergency,0.1");
+        final BrightnessThrottler throttler = createThrottlerSupported(data);
+
+        verify(mThermalServiceMock).registerThermalEventListenerWithType(
+                mThermalEventListenerCaptor.capture(), eq(Temperature.TYPE_SKIN));
+        final IThermalEventListener listener = mThermalEventListenerCaptor.getValue();
+
+        // Ensure that the multiple levels set via the string through the device config correctly
+        // override the original display device config ones.
+
+        // levelSevere
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(PowerManager.BRIGHTNESS_MAX, throttler.getBrightnessCap(), 0f);
+        assertFalse(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelSevere.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.9f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // levelCritical
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(0.9f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelCritical.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.5f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        //levelEmergency
+        // Set status too low to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus - 1));
+        mTestLooper.dispatchAll();
+        assertEquals(0.5f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+
+        // Set status high enough to trigger throttling
+        listener.notifyThrottling(getSkinTemp(levelEmergency.thermalStatus));
+        mTestLooper.dispatchAll();
+        assertEquals(0.1f, throttler.getBrightnessCap(), 0f);
+        assertTrue(throttler.isThrottled());
+    }
+
     private void assertThrottlingLevelsEquals(
             List<ThrottlingLevel> expected,
             List<ThrottlingLevel> actual) {
@@ -307,12 +473,13 @@
     }
 
     private BrightnessThrottler createThrottlerUnsupported() {
-        return new BrightnessThrottler(mInjectorMock, mHandler, null, () -> {});
+        return new BrightnessThrottler(mInjectorMock, mHandler, mHandler, null, () -> {}, null);
     }
 
     private BrightnessThrottler createThrottlerSupported(BrightnessThrottlingData data) {
         assertNotNull(data);
-        return new BrightnessThrottler(mInjectorMock, mHandler, data, () -> {});
+        return new BrightnessThrottler(mInjectorMock, mHandler, BackgroundThread.getHandler(),
+                data, () -> {}, "123");
     }
 
     private Temperature getSkinTemp(@ThrottlingStatus int status) {
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index 864f315..968e1d8 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -16,6 +16,7 @@
 
 package com.android.server.display;
 
+import static android.hardware.display.DisplayManager.DeviceConfig.KEY_BRIGHTNESS_THROTTLING_DATA;
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_HIGH_AMBIENT_BRIGHTNESS_THRESHOLDS;
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_HIGH_DISPLAY_BRIGHTNESS_THRESHOLDS;
 import static android.hardware.display.DisplayManager.DeviceConfig.KEY_FIXED_REFRESH_RATE_LOW_AMBIENT_BRIGHTNESS_THRESHOLDS;
@@ -1902,6 +1903,11 @@
                     KEY_REFRESH_RATE_IN_HBM_HDR, String.valueOf(fps));
         }
 
+        void setBrightnessThrottlingData(String brightnessThrottlingData) {
+            putPropertyAndNotify(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                    KEY_BRIGHTNESS_THROTTLING_DATA, brightnessThrottlingData);
+        }
+
         void setLowDisplayBrightnessThresholds(int[] brightnessThresholds) {
             String thresholds = toPropertyValue(brightnessThresholds);