Merge "Moves quick affordances into domain layer." into tm-qpr-dev
diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java
index 58ddd49..13934e5 100644
--- a/core/java/android/app/PropertyInvalidatedCache.java
+++ b/core/java/android/app/PropertyInvalidatedCache.java
@@ -310,6 +310,21 @@
     public static final String MODULE_TELEPHONY = "telephony";
 
     /**
+     * Constants that affect retries when the process is unable to write the property.
+     * The first constant is the number of times the process will attempt to set the
+     * property.  The second constant is the delay between attempts.
+     */
+
+    /**
+     * Wait 200ms between retry attempts and the retry limit is 5.  That gives a total possible
+     * delay of 1s, which should be less than ANR timeouts.  The goal is to have the system crash
+     * because the property could not be set (which is a condition that is easily recognized) and
+     * not crash because of an ANR (which can be confusing to debug).
+     */
+    private static final int PROPERTY_FAILURE_RETRY_DELAY_MILLIS = 200;
+    private static final int PROPERTY_FAILURE_RETRY_LIMIT = 5;
+
+    /**
      * Construct a system property that matches the rules described above.  The module is
      * one of the permitted values above.  The API is a string that is a legal Java simple
      * identifier.  The api is modified to conform to the system property style guide by
@@ -670,7 +685,33 @@
                 }
             }
         }
-        SystemProperties.set(name, Long.toString(val));
+        RuntimeException failure = null;
+        for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) {
+            try {
+                SystemProperties.set(name, Long.toString(val));
+                if (attempt > 0) {
+                    // This log is not guarded.  Based on known bug reports, it should
+                    // occur once a week or less.  The purpose of the log message is to
+                    // identify the retries as a source of delay that might be otherwise
+                    // be attributed to the cache itself.
+                    Log.w(TAG, "Nonce set after " + attempt + " tries");
+                }
+                return;
+            } catch (RuntimeException e) {
+                if (failure == null) {
+                    failure = e;
+                }
+                try {
+                    Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS);
+                } catch (InterruptedException x) {
+                    // Ignore this exception.  The desired delay is only approximate and
+                    // there is no issue if the sleep sometimes terminates early.
+                }
+            }
+        }
+        // This point is reached only if SystemProperties.set() fails at least once.
+        // Rethrow the first exception that was received.
+        throw failure;
     }
 
     // Set the nonce in a static context.  No handle is available.
diff --git a/core/java/android/hardware/camera2/CameraDevice.java b/core/java/android/hardware/camera2/CameraDevice.java
index 0f1b39c..df1c0d7 100644
--- a/core/java/android/hardware/camera2/CameraDevice.java
+++ b/core/java/android/hardware/camera2/CameraDevice.java
@@ -892,7 +892,7 @@
      * <tr><th colspan="7">Preview stabilization guaranteed stream configurations</th></tr>
      * <tr><th colspan="2" id="rb">Target 1</th><th colspan="2" id="rb">Target 2</th><th rowspan="2">Sample use case(s)</th> </tr>
      * <tr><th>Type</th><th id="rb">Max size</th><th>Type</th><th id="rb">Max size</th></tr>
-     * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code s1440p}</td><td colspan="4" id="rb"></td> <td>Stabilized preview, GPU video processing, or no-preview stabilized video recording.</td> </tr>
+     * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code s1440p}</td><td colspan="2" id="rb"></td> <td>Stabilized preview, GPU video processing, or no-preview stabilized video recording.</td> </tr>
      * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code s1440p}</td> <td>{@code JPEG / YUV}</td><td id="rb">{@code MAXIMUM }</td><td>Standard still imaging with stabilized preview.</td> </tr>
      * <tr> <td>{@code PRIV / YUV}</td><td id="rb">{@code PREVIEW}</td> <td>{@code PRIV / YUV}</td><td id="rb">{@code s1440p }</td><td>High-resolution recording with stabilized preview and recording stream.</td> </tr>
      * </table><br>
diff --git a/core/java/android/provider/DeviceConfig.java b/core/java/android/provider/DeviceConfig.java
index 13a3ec8..fd4b94a 100644
--- a/core/java/android/provider/DeviceConfig.java
+++ b/core/java/android/provider/DeviceConfig.java
@@ -623,7 +623,7 @@
     private static final List<String> PUBLIC_NAMESPACES =
             Arrays.asList(NAMESPACE_TEXTCLASSIFIER, NAMESPACE_RUNTIME, NAMESPACE_STATSD_JAVA,
                     NAMESPACE_STATSD_JAVA_BOOT, NAMESPACE_SELECTION_TOOLBAR, NAMESPACE_AUTOFILL,
-                    NAMESPACE_DEVICE_POLICY_MANAGER);
+                    NAMESPACE_DEVICE_POLICY_MANAGER, NAMESPACE_CONTENT_CAPTURE);
     /**
      * Privacy related properties definitions.
      *
diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java
index e246634..2c2ae06 100644
--- a/core/java/android/view/ViewStructure.java
+++ b/core/java/android/view/ViewStructure.java
@@ -45,6 +45,30 @@
 public abstract class ViewStructure {
 
     /**
+     * Key used for writing active child view information to the content capture bundle.
+     *
+     * The value stored under this key will be an ordered list of Autofill IDs of child views.
+     *
+     * TODO(b/241498401): Add @TestApi in Android U
+     * @hide
+     */
+    public static final String EXTRA_ACTIVE_CHILDREN_IDS =
+            "android.view.ViewStructure.extra.ACTIVE_CHILDREN_IDS";
+
+    /**
+     * Key used for writing the first active child's position to the content capture bundle.
+     *
+     * When active child view information is provided under the
+     * {@link #EXTRA_ACTIVE_CHILDREN_IDS}, the value stored under this key will be the
+     * 0-based position of the first child view in the list relative to the positions of child views
+     * in the containing View's dataset.
+     *
+     * TODO(b/241498401): Add @TestApi in Android U
+     * @hide */
+    public static final String EXTRA_FIRST_ACTIVE_POSITION =
+            "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION";
+
+    /**
      * Set the identifier for this view.
      *
      * @param id The view's identifier, as per {@link View#getId View.getId()}.
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 63d42c0..67352c0 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -4396,15 +4396,42 @@
                 changes |= LAYOUT_CHANGED;
             }
 
-            if (!Arrays.equals(paramsForRotation, o.paramsForRotation)) {
+            if (paramsForRotation != o.paramsForRotation) {
+                if ((changes & LAYOUT_CHANGED) == 0) {
+                    if (paramsForRotation != null && o.paramsForRotation != null
+                            && paramsForRotation.length == o.paramsForRotation.length) {
+                        for (int i = paramsForRotation.length - 1; i >= 0; i--) {
+                            if (hasLayoutDiff(paramsForRotation[i], o.paramsForRotation[i])) {
+                                changes |= LAYOUT_CHANGED;
+                                break;
+                            }
+                        }
+                    } else {
+                        changes |= LAYOUT_CHANGED;
+                    }
+                }
                 paramsForRotation = o.paramsForRotation;
                 checkNonRecursiveParams();
-                changes |= LAYOUT_CHANGED;
             }
 
             return changes;
         }
 
+        /**
+         * Returns {@code true} if the 2 params may have difference results of
+         * {@link WindowLayout#computeFrames}.
+         */
+        private static boolean hasLayoutDiff(LayoutParams a, LayoutParams b) {
+            return a.width != b.width || a.height != b.height || a.x != b.x || a.y != b.y
+                    || a.horizontalMargin != b.horizontalMargin
+                    || a.verticalMargin != b.verticalMargin
+                    || a.layoutInDisplayCutoutMode != b.layoutInDisplayCutoutMode
+                    || a.gravity != b.gravity || !Arrays.equals(a.providedInsets, b.providedInsets)
+                    || a.mFitInsetsTypes != b.mFitInsetsTypes
+                    || a.mFitInsetsSides != b.mFitInsetsSides
+                    || a.mFitInsetsIgnoringVisibility != b.mFitInsetsIgnoringVisibility;
+        }
+
         @Override
         public String debug(String output) {
             output += "Contents of " + this + ":";
diff --git a/core/java/android/view/contentcapture/ContentCaptureEvent.java b/core/java/android/view/contentcapture/ContentCaptureEvent.java
index ba4176f..db4ac5d 100644
--- a/core/java/android/view/contentcapture/ContentCaptureEvent.java
+++ b/core/java/android/view/contentcapture/ContentCaptureEvent.java
@@ -55,6 +55,15 @@
     /**
      * Called when a node has been added to the screen and is visible to the user.
      *
+     * On API level 33, this event may be re-sent with additional information if a view's children
+     * have changed, e.g. scrolling Views inside of a ListView. This information will be stored in
+     * the extras Bundle associated with the event's ViewNode. Within the Bundle, the
+     * "android.view.ViewStructure.extra.ACTIVE_CHILDREN_IDS" key may be used to get a list of
+     * Autofill IDs of active child views, and the
+     * "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION" key may be used to get the 0-based
+     * position of the first active child view in the list relative to the positions of child views
+     * in the container View's dataset.
+     *
      * <p>The metadata of the node is available through {@link #getViewNode()}.
      */
     public static final int TYPE_VIEW_APPEARED = 1;
diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java
index 48d2970..1664637 100644
--- a/core/java/android/view/contentcapture/ContentCaptureManager.java
+++ b/core/java/android/view/contentcapture/ContentCaptureManager.java
@@ -280,6 +280,15 @@
             "service_explicitly_enabled";
 
     /**
+     * Device config property used by {@code android.widget.AbsListView} to determine whether or
+     * not it should report the positions of its children to Content Capture.
+     *
+     * @hide
+     */
+    public static final String DEVICE_CONFIG_PROPERTY_REPORT_LIST_VIEW_CHILDREN =
+            "report_list_view_children";
+
+    /**
      * Maximum number of events that are buffered before sent to the app.
      *
      * @hide
diff --git a/core/java/android/widget/AbsListView.java b/core/java/android/widget/AbsListView.java
index 231ae08..0b0bfb1 100644
--- a/core/java/android/widget/AbsListView.java
+++ b/core/java/android/widget/AbsListView.java
@@ -20,6 +20,7 @@
 import android.annotation.DrawableRes;
 import android.annotation.NonNull;
 import android.annotation.TestApi;
+import android.app.ActivityThread;
 import android.compat.annotation.UnsupportedAppUsage;
 import android.content.Context;
 import android.content.Intent;
@@ -37,6 +38,7 @@
 import android.os.Parcelable;
 import android.os.StrictMode;
 import android.os.Trace;
+import android.provider.DeviceConfig;
 import android.text.Editable;
 import android.text.InputType;
 import android.text.TextUtils;
@@ -65,6 +67,7 @@
 import android.view.ViewGroup;
 import android.view.ViewHierarchyEncoder;
 import android.view.ViewParent;
+import android.view.ViewStructure;
 import android.view.ViewTreeObserver;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
@@ -73,6 +76,9 @@
 import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
 import android.view.animation.Interpolator;
 import android.view.animation.LinearInterpolator;
+import android.view.autofill.AutofillId;
+import android.view.contentcapture.ContentCaptureManager;
+import android.view.contentcapture.ContentCaptureSession;
 import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CorrectionInfo;
@@ -634,6 +640,23 @@
     private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;
 
     /**
+     * Indicates that reporting positions of child views to content capture is enabled via
+     * DeviceConfig.
+     */
+    private static boolean sContentCaptureReportingEnabledByDeviceConfig = false;
+
+    /**
+     * Listens for changes to DeviceConfig properties and updates stored values accordingly.
+     */
+    private static DeviceConfig.OnPropertiesChangedListener sDeviceConfigChangeListener = null;
+
+    /**
+     * Indicates that child positions of views should be reported to Content Capture the next time
+     * that active views are refreshed.
+     */
+    private boolean mReportChildrenToContentCaptureOnNextUpdate = true;
+
+    /**
      * Helper object that renders and controls the fast scroll thumb.
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 123768941)
@@ -850,8 +873,44 @@
         public void adjustListItemSelectionBounds(Rect bounds);
     }
 
+    private static class DeviceConfigChangeListener
+            implements DeviceConfig.OnPropertiesChangedListener {
+        @Override
+        public void onPropertiesChanged(
+                @NonNull DeviceConfig.Properties properties) {
+            if (!DeviceConfig.NAMESPACE_CONTENT_CAPTURE.equals(properties.getNamespace())) {
+                return;
+            }
+
+            for (String key : properties.getKeyset()) {
+                if (!ContentCaptureManager.DEVICE_CONFIG_PROPERTY_REPORT_LIST_VIEW_CHILDREN
+                        .equals(key)) {
+                    continue;
+                }
+
+                sContentCaptureReportingEnabledByDeviceConfig = properties.getBoolean(key,
+                        false);
+            }
+        }
+    }
+
+    private static void setupDeviceConfigProperties() {
+        if (sDeviceConfigChangeListener == null) {
+            sContentCaptureReportingEnabledByDeviceConfig = DeviceConfig.getBoolean(
+                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                    ContentCaptureManager.DEVICE_CONFIG_PROPERTY_REPORT_LIST_VIEW_CHILDREN,
+                    false);
+            sDeviceConfigChangeListener = new DeviceConfigChangeListener();
+            DeviceConfig.addOnPropertiesChangedListener(
+                    DeviceConfig.NAMESPACE_CONTENT_CAPTURE,
+                    ActivityThread.currentApplication().getMainExecutor(),
+                    sDeviceConfigChangeListener);
+        }
+    }
+
     public AbsListView(Context context) {
         super(context);
+        setupDeviceConfigProperties();
         mEdgeGlowBottom = new EdgeEffect(context);
         mEdgeGlowTop = new EdgeEffect(context);
         initAbsListView();
@@ -874,6 +933,7 @@
 
     public AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
         super(context, attrs, defStyleAttr, defStyleRes);
+        setupDeviceConfigProperties();
         mEdgeGlowBottom = new EdgeEffect(context, attrs);
         mEdgeGlowTop = new EdgeEffect(context, attrs);
         initAbsListView();
@@ -4699,6 +4759,14 @@
                 mOnScrollListener.onScrollStateChanged(this, newState);
             }
         }
+
+        // When scrolling, we want to report changes in the active children to Content Capture,
+        // so set the flag to report on the next update only when scrolling has stopped or a fling
+        // scroll is performed.
+        if (newState == OnScrollListener.SCROLL_STATE_IDLE
+                || newState == OnScrollListener.SCROLL_STATE_FLING) {
+            mReportChildrenToContentCaptureOnNextUpdate = true;
+        }
     }
 
     /**
@@ -6654,10 +6722,77 @@
         mRecycler.mRecyclerListener = listener;
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * This method will initialize the fields of the {@link ViewStructure}
+     * using the base implementation in {@link View}. On API level 33 and higher, it may also
+     * write information about the positions of active views to the extras bundle provided by the
+     * {@link ViewStructure}.
+     *
+     * NOTE: When overriding this method on API level 33, if not calling super() or if changing the
+     * logic for child views, be sure to provide values for the first active child view position and
+     * the list of active child views in the {@link ViewStructure}'s extras {@link Bundle} using the
+     * "android.view.ViewStructure.extra.ACTIVE_CHILDREN_IDS" and
+     * "android.view.ViewStructure.extra.FIRST_ACTIVE_POSITION" keys.
+     *
+     * @param structure {@link ViewStructure} to be filled in with structured view data.
+     * @param flags optional flags.
+     *
+     * @see View#AUTOFILL_FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
+     */
+    @Override
+    public void onProvideContentCaptureStructure(
+            @NonNull ViewStructure structure, int flags) {
+        super.onProvideContentCaptureStructure(structure, flags);
+        if (!sContentCaptureReportingEnabledByDeviceConfig) {
+            return;
+        }
+
+        Bundle extras = structure.getExtras();
+
+        if (extras == null) {
+            Log.wtf(TAG, "Unexpected null extras Bundle in ViewStructure");
+            return;
+        }
+
+        int childCount = getChildCount();
+        ArrayList<AutofillId> idsList = new ArrayList<>(childCount);
+
+        for (int i = 0; i < childCount; ++i) {
+            View activeView = getChildAt(i);
+            if (activeView == null) {
+                continue;
+            }
+
+            idsList.add(activeView.getAutofillId());
+        }
+
+        extras.putParcelableArrayList(ViewStructure.EXTRA_ACTIVE_CHILDREN_IDS,
+                idsList);
+
+        extras.putInt(ViewStructure.EXTRA_FIRST_ACTIVE_POSITION,
+                getFirstVisiblePosition());
+    }
+
+    private void reportActiveViewsToContentCapture() {
+        if (!sContentCaptureReportingEnabledByDeviceConfig) {
+            return;
+        }
+
+        ContentCaptureSession session = getContentCaptureSession();
+        if (session != null) {
+            ViewStructure structure = session.newViewStructure(this);
+            onProvideContentCaptureStructure(structure, /* flags= */ 0);
+            session.notifyViewAppeared(structure);
+        }
+    }
+
     class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver {
         @Override
         public void onChanged() {
             super.onChanged();
+            mReportChildrenToContentCaptureOnNextUpdate = true;
             if (mFastScroll != null) {
                 mFastScroll.onSectionsChanged();
             }
@@ -6666,6 +6801,7 @@
         @Override
         public void onInvalidated() {
             super.onInvalidated();
+            mReportChildrenToContentCaptureOnNextUpdate = true;
             if (mFastScroll != null) {
                 mFastScroll.onSectionsChanged();
             }
@@ -6984,6 +7120,11 @@
                     lp.scrappedFromPosition = firstActivePosition + i;
                 }
             }
+
+            if (mReportChildrenToContentCaptureOnNextUpdate && childCount > 0) {
+                AbsListView.this.reportActiveViewsToContentCapture();
+                mReportChildrenToContentCaptureOnNextUpdate = false;
+            }
         }
 
         /**
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index 3bffa89..e3430a6 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -269,6 +269,20 @@
     }
 
     /**
+     * Sets whether a task should be translucent. When {@code false}, the existing translucent of
+     * the task applies, but when {@code true} the task will be forced to be translucent.
+     * @hide
+     */
+    @NonNull
+    public WindowContainerTransaction setForceTranslucent(
+            @NonNull WindowContainerToken container, boolean forceTranslucent) {
+        Change chg = getOrCreateChange(container.asBinder());
+        chg.mForceTranslucent = forceTranslucent;
+        chg.mChangeMask |= Change.CHANGE_FORCE_TRANSLUCENT;
+        return this;
+    }
+
+    /**
      * Used in conjunction with a shell-transition call (usually finishTransition). This is
      * basically a message to the transition system that a particular task should NOT go into
      * PIP even though it normally would. This is to deal with some edge-case situations where
@@ -834,11 +848,13 @@
         public static final int CHANGE_BOUNDS_TRANSACTION_RECT = 1 << 4;
         public static final int CHANGE_IGNORE_ORIENTATION_REQUEST = 1 << 5;
         public static final int CHANGE_FORCE_NO_PIP = 1 << 6;
+        public static final int CHANGE_FORCE_TRANSLUCENT = 1 << 7;
 
         private final Configuration mConfiguration = new Configuration();
         private boolean mFocusable = true;
         private boolean mHidden = false;
         private boolean mIgnoreOrientationRequest = false;
+        private boolean mForceTranslucent = false;
 
         private int mChangeMask = 0;
         private @ActivityInfo.Config int mConfigSetMask = 0;
@@ -858,6 +874,7 @@
             mFocusable = in.readBoolean();
             mHidden = in.readBoolean();
             mIgnoreOrientationRequest = in.readBoolean();
+            mForceTranslucent = in.readBoolean();
             mChangeMask = in.readInt();
             mConfigSetMask = in.readInt();
             mWindowSetMask = in.readInt();
@@ -903,6 +920,9 @@
             if ((other.mChangeMask & CHANGE_IGNORE_ORIENTATION_REQUEST) != 0) {
                 mIgnoreOrientationRequest = other.mIgnoreOrientationRequest;
             }
+            if ((other.mChangeMask & CHANGE_FORCE_TRANSLUCENT) != 0) {
+                mForceTranslucent = other.mForceTranslucent;
+            }
             mChangeMask |= other.mChangeMask;
             if (other.mActivityWindowingMode >= 0) {
                 mActivityWindowingMode = other.mActivityWindowingMode;
@@ -953,6 +973,15 @@
             return mIgnoreOrientationRequest;
         }
 
+        /** Gets the requested force translucent state. */
+        public boolean getForceTranslucent() {
+            if ((mChangeMask & CHANGE_FORCE_TRANSLUCENT) == 0) {
+                throw new RuntimeException("Force translucent not set. "
+                        + "Check CHANGE_FORCE_TRANSLUCENT first");
+            }
+            return mForceTranslucent;
+        }
+
         public int getChangeMask() {
             return mChangeMask;
         }
@@ -1030,6 +1059,7 @@
             dest.writeBoolean(mFocusable);
             dest.writeBoolean(mHidden);
             dest.writeBoolean(mIgnoreOrientationRequest);
+            dest.writeBoolean(mForceTranslucent);
             dest.writeInt(mChangeMask);
             dest.writeInt(mConfigSetMask);
             dest.writeInt(mWindowSetMask);
diff --git a/core/java/com/android/internal/util/ScreenshotHelper.java b/core/java/com/android/internal/util/ScreenshotHelper.java
index 8af2450..b9243ec 100644
--- a/core/java/com/android/internal/util/ScreenshotHelper.java
+++ b/core/java/com/android/internal/util/ScreenshotHelper.java
@@ -219,8 +219,9 @@
                 throw new IllegalArgumentException("Bundle does not contain a hardware bitmap");
             }
 
-            HardwareBuffer buffer = bundle.getParcelable(KEY_BUFFER);
-            ParcelableColorSpace colorSpace = bundle.getParcelable(KEY_COLOR_SPACE);
+            HardwareBuffer buffer = bundle.getParcelable(KEY_BUFFER, HardwareBuffer.class);
+            ParcelableColorSpace colorSpace = bundle.getParcelable(KEY_COLOR_SPACE,
+                    ParcelableColorSpace.class);
 
             return Bitmap.wrapHardwareBuffer(Objects.requireNonNull(buffer),
                     colorSpace.getColorSpace());
diff --git a/core/res/res/layout/side_fps_toast.xml b/core/res/res/layout/side_fps_toast.xml
new file mode 100644
index 0000000..58b8cc9
--- /dev/null
+++ b/core/res/res/layout/side_fps_toast.xml
@@ -0,0 +1,43 @@
+<?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.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+              android:layout_width="wrap_content"
+              android:layout_height="wrap_content"
+              android:minWidth="350dp"
+              android:layout_gravity="center"
+              android:theme="?attr/alertDialogTheme">
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/fp_power_button_enrollment_title"
+        android:singleLine="true"
+        android:ellipsize="end"
+        android:paddingLeft="20dp"/>
+    <Space
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_weight="1"/>
+    <Button
+        android:id="@+id/turn_off_screen"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/fp_power_button_enrollment_button_text"
+        android:paddingRight="20dp"
+        style="?android:attr/buttonBarNegativeButtonStyle"
+        android:maxLines="1"/>
+</LinearLayout>
\ No newline at end of file
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index d13bc87..687c642 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3519,9 +3519,9 @@
          (for side fingerprint) -->
     <integer name="config_sidefpsPostAuthDowntime">400</integer>
 
-    <!-- The time (in millis) that a finger tap will wait for a power button
-         before dismissing the power dialog during enrollment(for side fingerprint) -->
-    <integer name="config_sidefpsEnrollPowerPressWindow">300</integer>
+    <!-- The time (in millis) the clickable toast dialog will last until
+         automatically dismissing. This is currently used in SideFpsEventHandler -->
+    <integer name="config_sideFpsToastTimeout">3000</integer>
 
     <!-- This config is used to force VoiceInteractionService to start on certain low ram devices.
          It declares the package name of VoiceInteractionService that should be started. -->
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index a3b05b5..e3b7100 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -3568,21 +3568,17 @@
     <!-- [CHAR LIMIT=NONE] Message to show in upgrading dialog when the bulk of the upgrade work is done. -->
     <string name="android_upgrading_complete">Finishing boot.</string>
 
-    <!-- [CHAR LIMIT=40] Title of dialog shown to confirm device going to sleep if the power button
-    is pressed during fingerprint enrollment. -->
-    <string name="fp_power_button_enrollment_title">Continue setup?</string>
-
     <!-- [CHAR LIMIT=NONE] Message of dialog shown to confirm device going to sleep if the power
     button is pressed during fingerprint enrollment. -->
     <string name="fp_power_button_enrollment_message">You pressed the power button — this usually turns off the screen.\n\nTry tapping lightly while setting up your fingerprint.</string>
 
+    <!-- [CHAR LIMIT=40] Title of dialog shown to confirm device going to sleep if the power button
+    is pressed during fingerprint enrollment. -->
+    <string name="fp_power_button_enrollment_title">Tap to turn off screen</string>
+
     <!-- [CHAR LIMIT=20] Positive button of dialog shown to confirm device going to sleep if the
     power button is pressed during fingerprint enrollment. -->
-    <string name="fp_power_button_enrollment_positive_button">Turn off screen</string>
-
-    <!-- [CHAR LIMIT=20] Negative button of dialog shown to confirm device going to sleep if the
-    power button is pressed during fingerprint enrollment. -->
-    <string name="fp_power_button_enrollment_negative_button">Continue setup</string>
+    <string name="fp_power_button_enrollment_button_text">Turn off screen</string>
 
     <!-- [CHAR LIMIT=40] Title of dialog shown to confirm device going to sleep if the power button
     is pressed during biometric prompt when a side fingerprint sensor is present. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index d7484c7..1192080 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1853,8 +1853,7 @@
   <java-symbol type="string" name="fp_power_button_bp_negative_button" />
   <java-symbol type="string" name="fp_power_button_enrollment_title" />
   <java-symbol type="string" name="fp_power_button_enrollment_message" />
-  <java-symbol type="string" name="fp_power_button_enrollment_positive_button" />
-  <java-symbol type="string" name="fp_power_button_enrollment_negative_button" />
+  <java-symbol type="string" name="fp_power_button_enrollment_button_text" />
   <java-symbol type="string" name="global_actions" />
   <java-symbol type="string" name="global_action_power_off" />
   <java-symbol type="string" name="global_action_power_options" />
@@ -2631,7 +2630,11 @@
   <java-symbol type="integer" name="config_sidefpsBpPowerPressWindow"/>
   <java-symbol type="integer" name="config_sidefpsKeyguardPowerPressWindow"/>
   <java-symbol type="integer" name="config_sidefpsPostAuthDowntime"/>
-  <java-symbol type="integer" name="config_sidefpsEnrollPowerPressWindow"/>
+  <java-symbol type="integer" name="config_sideFpsToastTimeout"/>
+
+  <!-- Clickable toast used during sidefps enrollment -->
+  <java-symbol type="layout" name="side_fps_toast" />
+  <java-symbol type="id" name="turn_off_screen" />
 
   <!-- Face authentication messages -->
   <java-symbol type="string" name="face_recalibrate_notification_name" />
diff --git a/core/tests/coretests/src/android/view/WindowParamsTest.java b/core/tests/coretests/src/android/view/WindowParamsTest.java
new file mode 100644
index 0000000..49d4872
--- /dev/null
+++ b/core/tests/coretests/src/android/view/WindowParamsTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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 android.view;
+
+import static org.junit.Assert.assertEquals;
+
+import android.os.Parcel;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+
+@Presubmit
+@SmallTest
+public class WindowParamsTest {
+
+    @Test
+    public void testParamsForRotation() {
+        final WindowManager.LayoutParams paramsA = new WindowManager.LayoutParams();
+        initParamsForRotation(paramsA);
+        final Parcel parcel = Parcel.obtain();
+        paramsA.writeToParcel(parcel, 0);
+        parcel.setDataPosition(0);
+
+        final WindowManager.LayoutParams paramsB = new WindowManager.LayoutParams(parcel);
+        assertEquals(0, paramsA.copyFrom(paramsB));
+
+        for (int i = 1; i <= 12; i++) {
+            initParamsForRotation(paramsA);
+            changeField(i, paramsA.paramsForRotation[0]);
+            assertEquals("Change not found for case " + i,
+                    WindowManager.LayoutParams.LAYOUT_CHANGED, paramsA.copyFrom(paramsB));
+        }
+
+        parcel.recycle();
+    }
+
+    private static void initParamsForRotation(WindowManager.LayoutParams params) {
+        params.paramsForRotation = new WindowManager.LayoutParams[4];
+        for (int i = 0; i < 4; i++) {
+            params.paramsForRotation[i] = new WindowManager.LayoutParams();
+        }
+    }
+
+    private static void changeField(int fieldCase, WindowManager.LayoutParams params) {
+        switch (fieldCase) {
+            case 1:
+                params.width++;
+                break;
+            case 2:
+                params.height++;
+                break;
+            case 3:
+                params.x++;
+                break;
+            case 4:
+                params.y++;
+                break;
+            case 5:
+                params.horizontalMargin++;
+                break;
+            case 6:
+                params.verticalMargin++;
+                break;
+            case 7:
+                params.layoutInDisplayCutoutMode++;
+                break;
+            case 8:
+                params.gravity++;
+                break;
+            case 9:
+                params.providedInsets = new InsetsFrameProvider[0];
+                break;
+            case 10:
+                params.setFitInsetsTypes(0);
+                break;
+            case 11:
+                params.setFitInsetsSides(0);
+                break;
+            case 12:
+                params.setFitInsetsIgnoringVisibility(!params.isFitInsetsIgnoringVisibility());
+                break;
+        }
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
index 312af4f..ee8c414 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/animation/PhysicsAnimator.kt
@@ -22,7 +22,6 @@
 import androidx.dynamicanimation.animation.DynamicAnimation
 import androidx.dynamicanimation.animation.FlingAnimation
 import androidx.dynamicanimation.animation.FloatPropertyCompat
-import androidx.dynamicanimation.animation.FrameCallbackScheduler
 import androidx.dynamicanimation.animation.SpringAnimation
 import androidx.dynamicanimation.animation.SpringForce
 
@@ -125,12 +124,6 @@
     private var defaultFling: FlingConfig = globalDefaultFling
 
     /**
-     * FrameCallbackScheduler to use if it need custom FrameCallbackScheduler, if this is null,
-     * it will use the default FrameCallbackScheduler in the DynamicAnimation.
-     */
-    private var customScheduler: FrameCallbackScheduler? = null
-
-    /**
      * Internal listeners that respond to DynamicAnimations updating and ending, and dispatch to
      * the listeners provided via [addUpdateListener] and [addEndListener]. This allows us to add
      * just one permanent update and end listener to the DynamicAnimations.
@@ -454,14 +447,6 @@
         this.defaultFling = defaultFling
     }
 
-    /**
-     * Set the custom FrameCallbackScheduler for all aniatmion in this animator. Set this with null for
-     * restoring to default FrameCallbackScheduler.
-     */
-    fun setCustomScheduler(scheduler: FrameCallbackScheduler) {
-        this.customScheduler = scheduler
-    }
-
     /** Starts the animations! */
     fun start() {
         startAction()
@@ -511,12 +496,9 @@
                     // springs) on this property before flinging.
                     cancel(animatedProperty)
 
-                    // Apply the custom animation scheduler if it not null
-                    val flingAnim = getFlingAnimation(animatedProperty, target)
-                    flingAnim.scheduler = customScheduler ?: flingAnim.scheduler
-
                     // Apply the configuration and start the animation.
-                    flingAnim.also { flingConfig.applyToAnimation(it) }.start()
+                    getFlingAnimation(animatedProperty, target)
+                        .also { flingConfig.applyToAnimation(it) }.start()
                 }
             }
 
@@ -529,18 +511,6 @@
                     // Apply the configuration and start the animation.
                     val springAnim = getSpringAnimation(animatedProperty, target)
 
-                    // If customScheduler is exist and has not been set to the animation,
-                    // it should set here.
-                    if (customScheduler != null &&
-                            springAnim.scheduler != customScheduler) {
-                        // Cancel the animation before set animation handler
-                        if (springAnim.isRunning) {
-                            cancel(animatedProperty)
-                        }
-                        // Apply the custom scheduler handler if it not null
-                        springAnim.scheduler = customScheduler ?: springAnim.scheduler
-                    }
-
                     // Apply the configuration and start the animation.
                     springConfig.applyToAnimation(springAnim)
                     animationStartActions.add(springAnim::start)
@@ -596,12 +566,9 @@
                                     }
                                 }
 
-                                // Apply the custom animation scheduler if it not null
-                                val springAnim = getSpringAnimation(animatedProperty, target)
-                                springAnim.scheduler = customScheduler ?: springAnim.scheduler
-
                                 // Apply the configuration and start the spring animation.
-                                springAnim.also { springConfig.applyToAnimation(it) }.start()
+                                getSpringAnimation(animatedProperty, target)
+                                    .also { springConfig.applyToAnimation(it) }.start()
                             }
                         }
                     })
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
index afc706e..b8204d0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenConstants.java
@@ -19,6 +19,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
 
 import android.annotation.IntDef;
 
@@ -55,4 +56,7 @@
             {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
     public static final int[] CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE =
             {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED, WINDOWING_MODE_MULTI_WINDOW};
+
+    /** Flag applied to a transition change to identify it as a divider bar for animation. */
+    public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
index bbaf51f..a6a04cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java
@@ -128,15 +128,9 @@
                 mainExecutor);
     }
 
-    // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
-    @BindsOptionalOf
-    @DynamicOverride
-    abstract DisplayImeController optionalDisplayImeController();
-
     @WMSingleton
     @Provides
     static DisplayImeController provideDisplayImeController(
-            @DynamicOverride Optional<DisplayImeController> overrideDisplayImeController,
             IWindowManager wmService,
             ShellInit shellInit,
             DisplayController displayController,
@@ -144,9 +138,6 @@
             TransactionPool transactionPool,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
-        if (overrideDisplayImeController.isPresent()) {
-            return overrideDisplayImeController.get();
-        }
         return new DisplayImeController(wmService, shellInit, displayController,
                 displayInsetsController, transactionPool, mainExecutor);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
index 81e49f8..b32c3ee 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java
@@ -29,7 +29,6 @@
 import android.app.TaskInfo;
 import android.content.Context;
 import android.graphics.Rect;
-import android.view.Choreographer;
 import android.view.Surface;
 import android.view.SurfaceControl;
 import android.window.TaskSnapshot;
@@ -279,14 +278,15 @@
             mEndValue = endValue;
             addListener(this);
             addUpdateListener(this);
-            mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+            mSurfaceControlTransactionFactory =
+                    new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
             mTransitionDirection = TRANSITION_DIRECTION_NONE;
         }
 
         @Override
         public void onAnimationStart(Animator animation) {
             mCurrentValue = mStartValue;
-            onStartTransaction(mLeash, newSurfaceControlTransaction());
+            onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction());
             if (mPipAnimationCallback != null) {
                 mPipAnimationCallback.onPipAnimationStart(mTaskInfo, this);
             }
@@ -294,14 +294,16 @@
 
         @Override
         public void onAnimationUpdate(ValueAnimator animation) {
-            applySurfaceControlTransaction(mLeash, newSurfaceControlTransaction(),
+            applySurfaceControlTransaction(mLeash,
+                    mSurfaceControlTransactionFactory.getTransaction(),
                     animation.getAnimatedFraction());
         }
 
         @Override
         public void onAnimationEnd(Animator animation) {
             mCurrentValue = mEndValue;
-            final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
+            final SurfaceControl.Transaction tx =
+                    mSurfaceControlTransactionFactory.getTransaction();
             onEndTransaction(mLeash, tx, mTransitionDirection);
             if (mPipAnimationCallback != null) {
                 mPipAnimationCallback.onPipAnimationEnd(mTaskInfo, tx, this);
@@ -348,7 +350,8 @@
         }
 
         void setColorContentOverlay(Context context) {
-            final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
+            final SurfaceControl.Transaction tx =
+                    mSurfaceControlTransactionFactory.getTransaction();
             if (mContentOverlay != null) {
                 mContentOverlay.detach(tx);
             }
@@ -357,7 +360,8 @@
         }
 
         void setSnapshotContentOverlay(TaskSnapshot snapshot, Rect sourceRectHint) {
-            final SurfaceControl.Transaction tx = newSurfaceControlTransaction();
+            final SurfaceControl.Transaction tx =
+                    mSurfaceControlTransactionFactory.getTransaction();
             if (mContentOverlay != null) {
                 mContentOverlay.detach(tx);
             }
@@ -406,7 +410,7 @@
         void setDestinationBounds(Rect destinationBounds) {
             mDestinationBounds.set(destinationBounds);
             if (mAnimationType == ANIM_TYPE_ALPHA) {
-                onStartTransaction(mLeash, newSurfaceControlTransaction());
+                onStartTransaction(mLeash, mSurfaceControlTransactionFactory.getTransaction());
             }
         }
 
@@ -441,16 +445,6 @@
             mEndValue = endValue;
         }
 
-        /**
-         * @return {@link SurfaceControl.Transaction} instance with vsync-id.
-         */
-        protected SurfaceControl.Transaction newSurfaceControlTransaction() {
-            final SurfaceControl.Transaction tx =
-                    mSurfaceControlTransactionFactory.getTransaction();
-            tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
-            return tx;
-        }
-
         @VisibleForTesting
         public void setSurfaceControlTransactionFactory(
                 PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
index 3ac08a6..b9746e3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java
@@ -20,6 +20,7 @@
 import android.graphics.Matrix;
 import android.graphics.Rect;
 import android.graphics.RectF;
+import android.view.Choreographer;
 import android.view.SurfaceControl;
 
 import com.android.wm.shell.R;
@@ -234,4 +235,18 @@
     public interface SurfaceControlTransactionFactory {
         SurfaceControl.Transaction getTransaction();
     }
+
+    /**
+     * Implementation of {@link SurfaceControlTransactionFactory} that returns
+     * {@link SurfaceControl.Transaction} with VsyncId being set.
+     */
+    public static class VsyncSurfaceControlTransactionFactory
+            implements SurfaceControlTransactionFactory {
+        @Override
+        public SurfaceControl.Transaction getTransaction() {
+            final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
+            tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
+            return tx;
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index f747b5e..b46eff6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -304,7 +304,8 @@
         mSurfaceTransactionHelper = surfaceTransactionHelper;
         mPipAnimationController = pipAnimationController;
         mPipUiEventLoggerLogger = pipUiEventLogger;
-        mSurfaceControlTransactionFactory = SurfaceControl.Transaction::new;
+        mSurfaceControlTransactionFactory =
+                new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory();
         mSplitScreenOptional = splitScreenOptional;
         mTaskOrganizer = shellTaskOrganizer;
         mMainExecutor = mainExecutor;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
index 5a21e07..44d2202 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMotionHelper.java
@@ -33,10 +33,6 @@
 import android.graphics.PointF;
 import android.graphics.Rect;
 import android.os.Debug;
-import android.os.Looper;
-import android.view.Choreographer;
-
-import androidx.dynamicanimation.animation.FrameCallbackScheduler;
 
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
@@ -89,25 +85,6 @@
     /** Coordinator instance for resolving conflicts with other floating content. */
     private FloatingContentCoordinator mFloatingContentCoordinator;
 
-    private ThreadLocal<FrameCallbackScheduler> mSfSchedulerThreadLocal =
-            ThreadLocal.withInitial(() -> {
-                final Looper initialLooper = Looper.myLooper();
-                final FrameCallbackScheduler scheduler = new FrameCallbackScheduler() {
-                    @Override
-                    public void postFrameCallback(@androidx.annotation.NonNull Runnable runnable) {
-                        // TODO(b/222697646): remove getSfInstance usage and use vsyncId for
-                        //  transactions
-                        Choreographer.getSfInstance().postFrameCallback(t -> runnable.run());
-                    }
-
-                    @Override
-                    public boolean isCurrentThread() {
-                        return Looper.myLooper() == initialLooper;
-                    }
-                };
-                return scheduler;
-            });
-
     /**
      * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()}
      * using physics animations.
@@ -210,10 +187,8 @@
     }
 
     public void init() {
-        // Note: Needs to get the shell main thread sf vsync animation handler
         mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance(
                 mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
-        mTemporaryBoundsPhysicsAnimator.setCustomScheduler(mSfSchedulerThreadLocal.get());
     }
 
     @NonNull
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 8e1ae39..4bc8e91 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -32,13 +32,13 @@
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.view.WindowManager.TRANSIT_TO_FRONT;
 import static android.view.WindowManager.transitTypeToString;
-import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
 import static android.window.TransitionInfo.FLAG_IS_DISPLAY;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER;
 
 import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER;
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES;
+import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT;
 import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED;
@@ -147,9 +147,6 @@
 
     private static final String TAG = StageCoordinator.class.getSimpleName();
 
-    /** Flag applied to a transition change to identify it as a divider bar for animation. */
-    public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM;
-
     private final SurfaceSession mSurfaceSession = new SurfaceSession();
 
     private final MainStage mMainStage;
@@ -894,6 +891,7 @@
             }
         });
         mShouldUpdateRecents = false;
+        mIsDividerRemoteAnimating = false;
 
         if (childrenToTop == null) {
             mSideStage.removeAllTasks(wct, false /* toTop */);
@@ -1808,7 +1806,8 @@
 
         boolean shouldAnimate = true;
         if (mSplitTransitions.isPendingEnter(transition)) {
-            shouldAnimate = startPendingEnterAnimation(transition, info, startTransaction);
+            shouldAnimate = startPendingEnterAnimation(
+                    transition, info, startTransaction, finishTransaction);
         } else if (mSplitTransitions.isPendingRecent(transition)) {
             shouldAnimate = startPendingRecentAnimation(transition, info, startTransaction);
         } else if (mSplitTransitions.isPendingDismiss(transition)) {
@@ -1836,7 +1835,8 @@
     }
 
     private boolean startPendingEnterAnimation(@NonNull IBinder transition,
-            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t) {
+            @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t,
+            @NonNull SurfaceControl.Transaction finishT) {
         // First, verify that we actually have opened apps in both splits.
         TransitionInfo.Change mainChild = null;
         TransitionInfo.Change sideChild = null;
@@ -1883,8 +1883,8 @@
                     + " before startAnimation().");
         }
 
-        finishEnterSplitScreen(t);
-        addDividerBarToTransition(info, t, true /* show */);
+        finishEnterSplitScreen(finishT);
+        addDividerBarToTransition(info, finishT, true /* show */);
         return true;
     }
 
@@ -1969,7 +1969,7 @@
             return false;
         }
 
-        addDividerBarToTransition(info, t, false /* show */);
+        addDividerBarToTransition(info, finishT, false /* show */);
         return true;
     }
 
@@ -1980,23 +1980,25 @@
     }
 
     private void addDividerBarToTransition(@NonNull TransitionInfo info,
-            @NonNull SurfaceControl.Transaction t, boolean show) {
+            @NonNull SurfaceControl.Transaction finishT, boolean show) {
         final SurfaceControl leash = mSplitLayout.getDividerLeash();
         final TransitionInfo.Change barChange = new TransitionInfo.Change(null /* token */, leash);
-        final Rect bounds = mSplitLayout.getDividerBounds();
-        barChange.setStartAbsBounds(bounds);
-        barChange.setEndAbsBounds(bounds);
+        mSplitLayout.getRefDividerBounds(mTempRect1);
+        barChange.setStartAbsBounds(mTempRect1);
+        barChange.setEndAbsBounds(mTempRect1);
         barChange.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK);
         barChange.setFlags(FLAG_IS_DIVIDER_BAR);
         // Technically this should be order-0, but this is running after layer assignment
         // and it's a special case, so just add to end.
         info.addChange(barChange);
-        // Be default, make it visible. The remote animator can adjust alpha if it plans to animate.
+
         if (show) {
-            t.setAlpha(leash, 1.f);
-            t.setLayer(leash, Integer.MAX_VALUE);
-            t.setPosition(leash, bounds.left, bounds.top);
-            t.show(leash);
+            finishT.setLayer(leash, Integer.MAX_VALUE);
+            finishT.setPosition(leash, mTempRect1.left, mTempRect1.top);
+            finishT.show(leash);
+            // Ensure divider surface are re-parented back into the hierarchy at the end of the
+            // transition. See Transition#buildFinishTransaction for more detail.
+            finishT.reparent(leash, mRootTaskLeash);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
index 5cce6b9..e26c259 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java
@@ -20,9 +20,9 @@
 import static android.view.WindowManager.TRANSIT_TO_BACK;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 
+import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
-import static com.android.wm.shell.splitscreen.StageCoordinator.FLAG_IS_DIVIDER_BAR;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
index a843b2a..45b69f1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/ScreenRotationAnimation.java
@@ -162,13 +162,12 @@
                     .setParent(mAnimLeash)
                     .setBLASTLayer()
                     .setSecure(screenshotBuffer.containsSecureLayers())
+                    .setOpaque(true)
                     .setCallsite("ShellRotationAnimation")
                     .setName("RotationLayer")
                     .build();
 
             t.setLayer(mAnimLeash, SCREEN_FREEZE_LAYER_BASE);
-            t.setPosition(mAnimLeash, 0, 0);
-            t.setAlpha(mAnimLeash, 1);
             t.show(mAnimLeash);
 
             final ColorSpace colorSpace = screenshotBuffer.getColorSpace();
@@ -181,6 +180,7 @@
                 mBackColorSurface = new SurfaceControl.Builder(session)
                         .setParent(rootLeash)
                         .setColorLayer()
+                        .setOpaque(true)
                         .setCallsite("ShellRotationAnimation")
                         .setName("BackColorSurface")
                         .build();
@@ -189,7 +189,6 @@
 
                 t.setLayer(mBackColorSurface, -1);
                 t.setColor(mBackColorSurface, new float[]{mStartLuma, mStartLuma, mStartLuma});
-                t.setAlpha(mBackColorSurface, 1);
                 t.show(mBackColorSurface);
             }
 
@@ -242,7 +241,6 @@
         t.setMatrix(mScreenshotLayer,
                 mTmpFloats[Matrix.MSCALE_X], mTmpFloats[Matrix.MSKEW_Y],
                 mTmpFloats[Matrix.MSKEW_X], mTmpFloats[Matrix.MSCALE_Y]);
-        t.setAlpha(mScreenshotLayer, (float) 1.0);
     }
 
     /**
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
index 514390f..d467b39 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayLayoutTest.java
@@ -47,6 +47,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.MockitoSession;
+import org.mockito.quality.Strictness;
 
 /**
  * Tests for {@link DisplayLayout}.
@@ -62,6 +63,7 @@
     public void setup() {
         mMockitoSession = mockitoSession()
                 .initMocks(this)
+                .strictness(Strictness.WARN)
                 .mockStatic(SystemBarUtils.class)
                 .startMocking();
     }
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index 70a7709..c972624 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -35,10 +35,20 @@
         app:layout_constraintBottom_toBottomOf="parent" />
 
     <LinearLayout
+        android:id="@+id/dream_overlay_extra_items"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        android:gravity="center_vertical"
+        android:layout_marginEnd="@dimen/dream_overlay_status_bar_extra_margin"
+        app:layout_constraintEnd_toStartOf="@+id/dream_overlay_system_status" />
+
+    <LinearLayout
         android:id="@+id/dream_overlay_system_status"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
         android:orientation="horizontal"
+        android:layout_marginStart="@dimen/dream_overlay_status_bar_extra_margin"
         app:layout_constraintEnd_toEndOf="parent">
 
         <com.android.systemui.statusbar.AlphaOptimizedImageView
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 3fb00a3..8ea2c0c 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1455,6 +1455,7 @@
     <dimen name="dream_overlay_camera_mic_off_indicator_size">8dp</dimen>
     <dimen name="dream_overlay_notification_indicator_size">6dp</dimen>
     <dimen name="dream_overlay_grey_chip_width">56dp</dimen>
+    <dimen name="dream_overlay_status_bar_extra_margin">16dp</dimen>
 
     <!-- Dream overlay complications related dimensions -->
     <dimen name="dream_overlay_complication_clock_time_text_size">100sp</dimen>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
index 203b236..7e42e1b 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/pip/PipSurfaceTransactionHelper.java
@@ -170,7 +170,7 @@
     /** @return {@link SurfaceControl.Transaction} instance with vsync-id */
     public static SurfaceControl.Transaction newSurfaceControlTransaction() {
         final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
-        tx.setFrameTimelineVsync(Choreographer.getSfInstance().getVsyncId());
+        tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId());
         return tx;
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
index 9265f07..33e8e35 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
@@ -122,12 +122,13 @@
                     IRemoteTransitionFinishedCallback finishCallback) {
                 final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
                 final RemoteAnimationTargetCompat[] appsCompat =
-                        RemoteAnimationTargetCompat.wrap(info, false /* wallpapers */, t, leashMap);
+                        RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
                 final RemoteAnimationTargetCompat[] wallpapersCompat =
-                        RemoteAnimationTargetCompat.wrap(info, true /* wallpapers */, t, leashMap);
-                // TODO(bc-unlock): Build wrapped object for non-apps target.
+                        RemoteAnimationTargetCompat.wrapNonApps(
+                                info, true /* wallpapers */, t, leashMap);
                 final RemoteAnimationTargetCompat[] nonAppsCompat =
-                        new RemoteAnimationTargetCompat[0];
+                        RemoteAnimationTargetCompat.wrapNonApps(
+                                info, false /* wallpapers */, t, leashMap);
 
                 // TODO(b/177438007): Move this set-up logic into launcher's animation impl.
                 boolean isReturnToHome = false;
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 ef9e095..7c3b5fc 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
@@ -16,7 +16,9 @@
 
 package com.android.systemui.shared.system;
 
+import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
 import static android.view.WindowManager.TRANSIT_TO_BACK;
@@ -24,6 +26,8 @@
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 
+import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
+
 import android.annotation.NonNull;
 import android.annotation.SuppressLint;
 import android.app.ActivityManager;
@@ -76,7 +80,7 @@
 
     private final SurfaceControl mStartLeash;
 
-    // Fields used only to unrap into RemoteAnimationTarget
+    // Fields used only to unwrap into RemoteAnimationTarget
     private final Rect startBounds;
 
     public final boolean willShowImeOnTarget;
@@ -203,8 +207,19 @@
 
     public RemoteAnimationTargetCompat(TransitionInfo.Change change, int order,
             TransitionInfo info, SurfaceControl.Transaction t) {
-        taskId = change.getTaskInfo() != null ? change.getTaskInfo().taskId : -1;
         mode = newModeToLegacyMode(change.getMode());
+        taskInfo = change.getTaskInfo();
+        if (taskInfo != null) {
+            taskId = taskInfo.taskId;
+            isNotInRecents = !taskInfo.isRunning;
+            activityType = taskInfo.getActivityType();
+            windowConfiguration = taskInfo.configuration.windowConfiguration;
+        } else {
+            taskId = INVALID_TASK_ID;
+            isNotInRecents = true;
+            activityType = ACTIVITY_TYPE_UNDEFINED;
+            windowConfiguration = new WindowConfiguration();
+        }
 
         // TODO: once we can properly sync transactions across process, then get rid of this leash.
         leash = createLeash(info, change, order, t);
@@ -221,22 +236,12 @@
         prefixOrderIndex = order;
         // TODO(shell-transitions): I guess we need to send content insets? evaluate how its used.
         contentInsets = new Rect(0, 0, 0, 0);
-        if (change.getTaskInfo() != null) {
-            isNotInRecents = !change.getTaskInfo().isRunning;
-            activityType = change.getTaskInfo().getActivityType();
-        } else {
-            isNotInRecents = true;
-            activityType = ACTIVITY_TYPE_UNDEFINED;
-        }
-        taskInfo = change.getTaskInfo();
         allowEnterPip = change.getAllowEnterPip();
         mStartLeash = null;
         rotationChange = change.getEndRotation() - change.getStartRotation();
-        windowType = INVALID_WINDOW_TYPE;
+        windowType = (change.getFlags() & FLAG_IS_DIVIDER_BAR) != 0
+                ? TYPE_DOCK_DIVIDER : INVALID_WINDOW_TYPE;
 
-        windowConfiguration = change.getTaskInfo() != null
-            ? change.getTaskInfo().configuration.windowConfiguration
-            : new WindowConfiguration();
         startBounds = change.getStartAbsBounds();
         willShowImeOnTarget = (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0;
     }
@@ -251,37 +256,62 @@
     }
 
     /**
-     * Represents a TransitionInfo object as an array of old-style targets
+     * Represents a TransitionInfo object as an array of old-style app targets
+     *
+     * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
+     *                 populated by this function. If null, it is ignored.
+     */
+    public static RemoteAnimationTargetCompat[] wrapApps(TransitionInfo info,
+            SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
+        final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
+        final SparseArray<TransitionInfo.Change> childTaskTargets = new SparseArray<>();
+        for (int i = 0; i < info.getChanges().size(); i++) {
+            final TransitionInfo.Change change = info.getChanges().get(i);
+            if (change.getTaskInfo() == null) continue;
+
+            final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
+            // Children always come before parent since changes are in top-to-bottom z-order.
+            if (taskInfo != null) {
+                if (childTaskTargets.contains(taskInfo.taskId)) {
+                    // has children, so not a leaf. Skip.
+                    continue;
+                }
+                if (taskInfo.hasParentTask()) {
+                    childTaskTargets.put(taskInfo.parentTaskId, change);
+                }
+            }
+
+            final RemoteAnimationTargetCompat targetCompat =
+                    new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
+            if (leashMap != null) {
+                leashMap.put(change.getLeash(), targetCompat.leash);
+            }
+            out.add(targetCompat);
+        }
+
+        return out.toArray(new RemoteAnimationTargetCompat[out.size()]);
+    }
+
+    /**
+     * Represents a TransitionInfo object as an array of old-style non-app targets
      *
      * @param wallpapers If true, this will return wallpaper targets; otherwise it returns
      *                   non-wallpaper targets.
      * @param leashMap Temporary map of change leash -> launcher leash. Is an output, so should be
      *                 populated by this function. If null, it is ignored.
      */
-    public static RemoteAnimationTargetCompat[] wrap(TransitionInfo info, boolean wallpapers,
+    public static RemoteAnimationTargetCompat[] wrapNonApps(TransitionInfo info, boolean wallpapers,
             SurfaceControl.Transaction t, ArrayMap<SurfaceControl, SurfaceControl> leashMap) {
         final ArrayList<RemoteAnimationTargetCompat> out = new ArrayList<>();
-        final SparseArray<TransitionInfo.Change> childTaskTargets = new SparseArray<>();
+
         for (int i = 0; i < info.getChanges().size(); i++) {
             final TransitionInfo.Change change = info.getChanges().get(i);
+            if (change.getTaskInfo() != null) continue;
+
             final boolean changeIsWallpaper =
                     (change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0;
             if (wallpapers != changeIsWallpaper) continue;
 
-            if (!wallpapers) {
-                final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
-                // Children always come before parent since changes are in top-to-bottom z-order.
-                if (taskInfo != null) {
-                    if (childTaskTargets.contains(taskInfo.taskId)) {
-                        // has children, so not a leaf. Skip.
-                        continue;
-                    }
-                    if (taskInfo.hasParentTask()) {
-                        childTaskTargets.put(taskInfo.parentTaskId, change);
-                    }
-                }
-            }
-
             final RemoteAnimationTargetCompat targetCompat =
                     new RemoteAnimationTargetCompat(change, info.getChanges().size() - i, info, t);
             if (leashMap != null) {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
index 7c1ef8c..f679225 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RemoteTransitionCompat.java
@@ -128,9 +128,10 @@
                     IRemoteTransitionFinishedCallback finishedCallback) {
                 final ArrayMap<SurfaceControl, SurfaceControl> leashMap = new ArrayMap<>();
                 final RemoteAnimationTargetCompat[] apps =
-                        RemoteAnimationTargetCompat.wrap(info, false /* wallpapers */, t, leashMap);
+                        RemoteAnimationTargetCompat.wrapApps(info, t, leashMap);
                 final RemoteAnimationTargetCompat[] wallpapers =
-                        RemoteAnimationTargetCompat.wrap(info, true /* wallpapers */, t, leashMap);
+                        RemoteAnimationTargetCompat.wrapNonApps(
+                                info, true /* wallpapers */, t, leashMap);
                 // TODO(b/177438007): Move this set-up logic into launcher's animation impl.
                 mToken = transition;
                 // This transition is for opening recents, so recents is on-top. We want to draw
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
index 78a45f9..b6923a8 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/WMComponent.java
@@ -75,25 +75,12 @@
      * Initializes all the WMShell components before starting any of the SystemUI components.
      */
     default void init() {
-        // TODO(238217847): To be removed once the dependencies are inverted and ShellController can
-        // inject these classes directly, otherwise, it's currently needed to ensure that these
-        // classes are created and set on the controller before onInit() is called
-        getShellInit();
-        getShellCommandHandler();
         getShell().onInit();
     }
 
     @WMSingleton
     ShellInterface getShell();
 
-    // TODO(238217847): To be removed once ShellController can inject ShellInit directly
-    @WMSingleton
-    ShellInit getShellInit();
-
-    // TODO(238217847): To be removed once ShellController can inject ShellCommandHandler directly
-    @WMSingleton
-    ShellCommandHandler getShellCommandHandler();
-
     @WMSingleton
     Optional<OneHanded> getOneHanded();
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProvider.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProvider.java
new file mode 100644
index 0000000..193d6f5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProvider.java
@@ -0,0 +1,117 @@
+/*
+ * 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.dreams;
+
+import android.view.View;
+
+import androidx.annotation.NonNull;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.statusbar.policy.CallbackController;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+/**
+ * {@link DreamOverlayStatusBarItemsProvider} provides extra dream overlay status bar items. A
+ * callback can be registered that will be informed of items being added or removed from the
+ * provider.
+ */
+@SysUISingleton
+public class DreamOverlayStatusBarItemsProvider implements
+        CallbackController<DreamOverlayStatusBarItemsProvider.Callback> {
+    /**
+     * Represents one item in the dream overlay status bar.
+     */
+    public interface StatusBarItem {
+        /**
+         * Return the {@link View} associated with this item.
+         */
+        View getView();
+    }
+
+    /**
+     * A callback to be registered with the provider to be informed of when the list of status bar
+     * items has changed.
+     */
+    public interface Callback {
+        /**
+         * Inform the callback that status bar items have changed.
+         */
+        void onStatusBarItemsChanged(List<StatusBarItem> newItems);
+    }
+
+    private final Executor mExecutor;
+    private final List<StatusBarItem> mItems = new ArrayList<>();
+    private final List<Callback> mCallbacks = new ArrayList<>();
+
+    @Inject
+    public DreamOverlayStatusBarItemsProvider(@Main Executor executor) {
+        mExecutor = executor;
+    }
+
+    @Override
+    public void addCallback(@NonNull Callback callback) {
+        mExecutor.execute(() -> {
+            Objects.requireNonNull(callback, "Callback must not be null.");
+            if (mCallbacks.contains(callback)) {
+                return;
+            }
+
+            mCallbacks.add(callback);
+            if (!mItems.isEmpty()) {
+                callback.onStatusBarItemsChanged(mItems);
+            }
+        });
+    }
+
+    @Override
+    public void removeCallback(@NonNull Callback callback) {
+        mExecutor.execute(() -> {
+            Objects.requireNonNull(callback, "Callback must not be null.");
+            mCallbacks.remove(callback);
+        });
+    }
+
+    /**
+     * Adds an item to the dream overlay status bar.
+     */
+    public void addStatusBarItem(StatusBarItem item) {
+        mExecutor.execute(() -> {
+            if (!mItems.contains(item)) {
+                mItems.add(item);
+                mCallbacks.forEach(callback -> callback.onStatusBarItemsChanged(mItems));
+            }
+        });
+    }
+
+    /**
+     * Removes an item from the dream overlay status bar.
+     */
+    public void removeStatusBarItem(StatusBarItem item) {
+        mExecutor.execute(() -> {
+            if (mItems.remove(item)) {
+                mCallbacks.forEach(callback -> callback.onStatusBarItemsChanged(mItems));
+            }
+        });
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
index a25257d..7e4a108 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarView.java
@@ -21,6 +21,7 @@
 import android.content.Context;
 import android.util.AttributeSet;
 import android.view.View;
+import android.view.ViewGroup;
 
 import androidx.constraintlayout.widget.ConstraintLayout;
 
@@ -29,6 +30,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
@@ -58,6 +60,7 @@
     public static final int STATUS_ICON_PRIORITY_MODE_ON = 6;
 
     private final Map<Integer, View> mStatusIcons = new HashMap<>();
+    private ViewGroup mSystemStatusViewGroup;
 
     public DreamOverlayStatusBarView(Context context) {
         this(context, null);
@@ -94,6 +97,8 @@
                 fetchStatusIconForResId(R.id.dream_overlay_notification_indicator));
         mStatusIcons.put(STATUS_ICON_PRIORITY_MODE_ON,
                 fetchStatusIconForResId(R.id.dream_overlay_priority_mode));
+
+        mSystemStatusViewGroup = findViewById(R.id.dream_overlay_extra_items);
     }
 
     void showIcon(@StatusIconType int iconType, boolean show, @Nullable String contentDescription) {
@@ -107,6 +112,11 @@
         icon.setVisibility(show ? View.VISIBLE : View.GONE);
     }
 
+    void setExtraStatusBarItemViews(List<View> views) {
+        mSystemStatusViewGroup.removeAllViews();
+        views.forEach(view -> mSystemStatusViewGroup.addView(view));
+    }
+
     private View fetchStatusIconForResId(int resId) {
         final View statusIcon = findViewById(resId);
         return Objects.requireNonNull(statusIcon);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index 55c1806..65cfae1 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -38,6 +38,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dreams.DreamOverlayStatusBarItemsProvider.StatusBarItem;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 import com.android.systemui.statusbar.policy.IndividualSensorPrivacyController;
 import com.android.systemui.statusbar.policy.NextAlarmController;
@@ -47,10 +48,13 @@
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.time.DateFormatUtil;
 
+import java.util.ArrayList;
+import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 
@@ -69,7 +73,10 @@
     private final Optional<DreamOverlayNotificationCountProvider>
             mDreamOverlayNotificationCountProvider;
     private final ZenModeController mZenModeController;
+    private final DreamOverlayStatusBarItemsProvider mStatusBarItemsProvider;
     private final Executor mMainExecutor;
+    private final List<DreamOverlayStatusBarItemsProvider.StatusBarItem> mExtraStatusBarItems =
+            new ArrayList<>();
 
     private boolean mIsAttached;
 
@@ -116,6 +123,9 @@
                             ? buildNotificationsContentDescription(notificationCount)
                             : null);
 
+    private final DreamOverlayStatusBarItemsProvider.Callback mStatusBarItemsProviderCallback =
+            this::onStatusBarItemsChanged;
+
     @Inject
     public DreamOverlayStatusBarViewController(
             DreamOverlayStatusBarView view,
@@ -129,7 +139,8 @@
             IndividualSensorPrivacyController sensorPrivacyController,
             Optional<DreamOverlayNotificationCountProvider> dreamOverlayNotificationCountProvider,
             ZenModeController zenModeController,
-            StatusBarWindowStateController statusBarWindowStateController) {
+            StatusBarWindowStateController statusBarWindowStateController,
+            DreamOverlayStatusBarItemsProvider statusBarItemsProvider) {
         super(view);
         mResources = resources;
         mMainExecutor = mainExecutor;
@@ -140,6 +151,7 @@
         mDateFormatUtil = dateFormatUtil;
         mSensorPrivacyController = sensorPrivacyController;
         mDreamOverlayNotificationCountProvider = dreamOverlayNotificationCountProvider;
+        mStatusBarItemsProvider = statusBarItemsProvider;
         mZenModeController = zenModeController;
 
         // Register to receive show/hide updates for the system status bar. Our custom status bar
@@ -166,6 +178,8 @@
         mDreamOverlayNotificationCountProvider.ifPresent(
                 provider -> provider.addCallback(mNotificationCountCallback));
 
+        mStatusBarItemsProvider.addCallback(mStatusBarItemsProviderCallback);
+
         mTouchInsetSession.addViewToTracking(mView);
     }
 
@@ -177,6 +191,7 @@
         mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
         mDreamOverlayNotificationCountProvider.ifPresent(
                 provider -> provider.removeCallback(mNotificationCountCallback));
+        mStatusBarItemsProvider.removeCallback(mStatusBarItemsProviderCallback);
         mTouchInsetSession.clear();
 
         mIsAttached = false;
@@ -271,4 +286,16 @@
             }
         });
     }
+
+    private void onStatusBarItemsChanged(List<StatusBarItem> newItems) {
+        mMainExecutor.execute(() -> {
+            mExtraStatusBarItems.clear();
+            mExtraStatusBarItems.addAll(newItems);
+            mView.setExtraStatusBarItemViews(
+                    newItems
+                            .stream()
+                            .map(StatusBarItem::getView)
+                            .collect(Collectors.toList()));
+        });
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
index d701f33..c790cfe 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.qs
 
 import android.content.Intent
+import android.content.res.Configuration
 import android.os.Handler
 import android.os.UserManager
 import android.provider.Settings
@@ -38,9 +39,11 @@
 import com.android.systemui.qs.dagger.QSScope
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.MultiUserSwitchController
+import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.statusbar.policy.UserInfoController
 import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener
+import com.android.systemui.util.LargeScreenUtils
 import com.android.systemui.util.ViewController
 import com.android.systemui.util.settings.GlobalSettings
 import javax.inject.Inject
@@ -69,18 +72,43 @@
     private val uiEventLogger: UiEventLogger,
     @Named(PM_LITE_ENABLED) private val showPMLiteButton: Boolean,
     private val globalSetting: GlobalSettings,
-    private val handler: Handler
+    private val handler: Handler,
+    private val configurationController: ConfigurationController,
 ) : ViewController<FooterActionsView>(view) {
 
     private var globalActionsDialog: GlobalActionsDialogLite? = null
 
     private var lastExpansion = -1f
     private var listening: Boolean = false
+    private var inSplitShade = false
 
-    private val alphaAnimator = TouchAnimator.Builder()
-            .addFloat(mView, "alpha", 0f, 1f)
-            .setStartDelay(0.9f)
+    private val singleShadeAnimator by lazy {
+        // In single shade, the actions footer should only appear at the end of the expansion,
+        // so that it doesn't overlap with the notifications panel.
+        TouchAnimator.Builder().addFloat(mView, "alpha", 0f, 1f).setStartDelay(0.9f).build()
+    }
+
+    private val splitShadeAnimator by lazy {
+        // The Actions footer view has its own background which is the same color as the qs panel's
+        // background.
+        // We don't want it to fade in at the same time as the rest of the panel, otherwise it is
+        // more opaque than the rest of the panel's background. Only applies to split shade.
+        val alphaAnimator = TouchAnimator.Builder().addFloat(mView, "alpha", 0f, 1f).build()
+        val bgAlphaAnimator =
+            TouchAnimator.Builder()
+                .addFloat(mView, "backgroundAlpha", 0f, 1f)
+                .setStartDelay(0.9f)
+                .build()
+        // In split shade, we want the actions footer to fade in exactly at the same time as the
+        // rest of the shade, as there is no overlap.
+        TouchAnimator.Builder()
+            .addFloat(alphaAnimator, "position", 0f, 1f)
+            .addFloat(bgAlphaAnimator, "position", 0f, 1f)
             .build()
+    }
+
+    private val animators: TouchAnimator
+        get() = if (inSplitShade) splitShadeAnimator else singleShadeAnimator
 
     var visible = true
         set(value) {
@@ -95,9 +123,7 @@
     private val multiUserSwitchController = multiUserSwitchControllerFactory.create(view)
 
     @VisibleForTesting
-    internal val securityFootersSeparator = View(context).apply {
-        visibility = View.GONE
-    }
+    internal val securityFootersSeparator = View(context).apply { visibility = View.GONE }
 
     private val onUserInfoChangedListener = OnUserInfoChangedListener { _, picture, _ ->
         val isGuestUser: Boolean = userManager.isGuestUser(KeyguardUpdateMonitor.getCurrentUser())
@@ -133,6 +159,17 @@
         }
     }
 
+    private val configurationListener =
+        object : ConfigurationController.ConfigurationListener {
+            override fun onConfigChanged(newConfig: Configuration?) {
+                updateResources()
+            }
+        }
+
+    private fun updateResources() {
+        inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(resources)
+    }
+
     override fun onInit() {
         multiUserSwitchController.init()
         securityFooterController.init()
@@ -189,6 +226,9 @@
         securityFooterController.setOnVisibilityChangedListener(visibilityListener)
         fgsManagerFooterController.setOnVisibilityChangedListener(visibilityListener)
 
+        configurationController.addCallback(configurationListener)
+
+        updateResources()
         updateView()
     }
 
@@ -201,6 +241,7 @@
         globalActionsDialog = null
         setListening(false)
         multiUserSetting.isListening = false
+        configurationController.removeCallback(configurationListener)
     }
 
     fun setListening(listening: Boolean) {
@@ -224,7 +265,7 @@
     }
 
     fun setExpansion(headerExpansionFraction: Float) {
-        alphaAnimator.setPosition(headerExpansionFraction)
+        animators.setPosition(headerExpansionFraction)
     }
 
     fun setKeyguardShowing(showing: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt
index 05038b7..309ac2a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt
@@ -27,6 +27,7 @@
 import android.view.View
 import android.widget.ImageView
 import android.widget.LinearLayout
+import androidx.annotation.Keep
 import com.android.settingslib.Utils
 import com.android.settingslib.drawable.UserIconDrawable
 import com.android.systemui.R
@@ -45,6 +46,19 @@
     private var qsDisabled = false
     private var expansionAmount = 0f
 
+    /**
+     * Sets the alpha of the background of this view.
+     *
+     * Used from a [TouchAnimator] in the controller.
+     */
+    var backgroundAlpha: Float = 1f
+        @Keep
+        set(value) {
+            field = value
+            background?.alpha = (value * 255).toInt()
+        }
+        @Keep get
+
     override fun onFinishInflate() {
         super.onFinishInflate()
         settingsContainer = findViewById(R.id.settings_button_container)
@@ -117,4 +131,4 @@
 private const val TAG = "FooterActionsView"
 private val VERBOSE = Log.isLoggable(TAG, Log.VERBOSE)
 private val MotionEvent.string
-    get() = "($id): ($x,$y)"
\ No newline at end of file
+    get() = "($id): ($x,$y)"
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
index 8c5e6cc..139fb8b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
@@ -557,9 +557,9 @@
     public void setQsExpansion(float expansion, float panelExpansionFraction,
             float proposedTranslation, float squishinessFraction) {
         float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation;
-        float progress = mTransitioningToFullShade || mState == StatusBarState.KEYGUARD
+        float alphaProgress = mTransitioningToFullShade || mState == StatusBarState.KEYGUARD
                 ? mFullShadeProgress : panelExpansionFraction;
-        setAlphaAnimationProgress(mInSplitShade ? progress : 1);
+        setAlphaAnimationProgress(mInSplitShade ? alphaProgress : 1);
         mContainer.setExpansion(expansion);
         final float translationScaleY = (mInSplitShade
                 ? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1);
@@ -600,7 +600,9 @@
         }
         mQSPanelController.setIsOnKeyguard(onKeyguard);
         mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
-        mQSFooterActionController.setExpansion(onKeyguardAndExpanded ? 1 : expansion);
+        float footerActionsExpansion =
+                onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion;
+        mQSFooterActionController.setExpansion(footerActionsExpansion);
         mQSPanelController.setRevealExpansion(expansion);
         mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
         mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 324c019..7155626 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -128,6 +128,8 @@
         if (mUsingMediaPlayer) {
             mHorizontalLinearLayout = new RemeasuringLinearLayout(mContext);
             mHorizontalLinearLayout.setOrientation(LinearLayout.HORIZONTAL);
+            mHorizontalLinearLayout.setVisibility(
+                    mUsingHorizontalLayout ? View.VISIBLE : View.GONE);
             mHorizontalLinearLayout.setClipChildren(false);
             mHorizontalLinearLayout.setClipToPadding(false);
 
@@ -445,6 +447,8 @@
         mMediaHostView = hostView;
         ViewGroup newParent = horizontal ? mHorizontalLinearLayout : this;
         ViewGroup currentParent = (ViewGroup) hostView.getParent();
+        Log.d(getDumpableTag(), "Reattaching media host: " + horizontal
+                + ", current " + currentParent + ", new " + newParent);
         if (currentParent != newParent) {
             if (currentParent != null) {
                 currentParent.removeView(hostView);
@@ -589,6 +593,7 @@
 
     void setUsingHorizontalLayout(boolean horizontal, ViewGroup mediaHostView, boolean force) {
         if (horizontal != mUsingHorizontalLayout || force) {
+            Log.d(getDumpableTag(), "setUsingHorizontalLayout: " + horizontal + ", " + force);
             mUsingHorizontalLayout = horizontal;
             ViewGroup newParent = horizontal ? mHorizontalContentContainer : this;
             switchAllContentToParent(newParent, mTileLayout);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index ec61ea6..6d5f844 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -88,6 +88,8 @@
                 public void onConfigurationChange(Configuration newConfig) {
                     mShouldUseSplitNotificationShade =
                             LargeScreenUtils.shouldUseSplitNotificationShade(getResources());
+                    mQSLogger.logOnConfigurationChanged(mLastOrientation, newConfig.orientation,
+                            mView.getDumpableTag());
                     onConfigurationChanged();
                     if (newConfig.orientation != mLastOrientation) {
                         mLastOrientation = newConfig.orientation;
@@ -164,6 +166,7 @@
         mHost.addCallback(mQSHostCallback);
         setTiles();
         mLastOrientation = getResources().getConfiguration().orientation;
+        mQSLogger.logOnViewAttached(mLastOrientation, mView.getDumpableTag());
         switchTileLayout(true);
 
         mDumpManager.registerDumpable(mView.getDumpableTag(), this);
@@ -171,6 +174,7 @@
 
     @Override
     protected void onViewDetached() {
+        mQSLogger.logOnViewDetached(mLastOrientation, mView.getDumpableTag());
         mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener);
         mHost.removeCallback(mQSHostCallback);
 
@@ -325,6 +329,8 @@
         /* Whether or not the panel currently contains a media player. */
         boolean horizontal = shouldUseHorizontalLayout();
         if (horizontal != mUsingHorizontalLayout || force) {
+            mQSLogger.logSwitchTileLayout(horizontal, mUsingHorizontalLayout, force,
+                    mView.getDumpableTag());
             mUsingHorizontalLayout = horizontal;
             mView.setUsingHorizontalLayout(mUsingHorizontalLayout, mMediaHost.getHostView(), force);
             updateMediaDisappearParameters();
diff --git a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
index ab795fa..948fb14 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/logging/QSLogger.kt
@@ -155,6 +155,54 @@
         })
     }
 
+    fun logOnViewAttached(orientation: Int, containerName: String) {
+        log(DEBUG, {
+            str1 = containerName
+            int1 = orientation
+        }, {
+            "onViewAttached: $str1 orientation $int1"
+        })
+    }
+
+    fun logOnViewDetached(orientation: Int, containerName: String) {
+        log(DEBUG, {
+            str1 = containerName
+            int1 = orientation
+        }, {
+            "onViewDetached: $str1 orientation $int1"
+        })
+    }
+
+    fun logOnConfigurationChanged(
+        lastOrientation: Int,
+        newOrientation: Int,
+        containerName: String
+    ) {
+        log(DEBUG, {
+            str1 = containerName
+            int1 = lastOrientation
+            int2 = newOrientation
+        }, {
+            "configuration change: $str1 orientation was $int1, now $int2"
+        })
+    }
+
+    fun logSwitchTileLayout(
+        after: Boolean,
+        before: Boolean,
+        force: Boolean,
+        containerName: String
+    ) {
+        log(DEBUG, {
+            str1 = containerName
+            bool1 = after
+            bool2 = before
+            bool3 = force
+        }, {
+            "change tile layout: $str1 horizontal=$bool1 (was $bool2), force? $bool3"
+        })
+    }
+
     private fun toStateString(state: Int): String {
         return when (state) {
             Tile.STATE_ACTIVE -> "active"
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
index 13a5615..2a46776 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationsQSContainerController.kt
@@ -6,7 +6,11 @@
 import android.view.WindowInsets
 import androidx.annotation.VisibleForTesting
 import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.*
+import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
+import androidx.constraintlayout.widget.ConstraintSet.END
+import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
+import androidx.constraintlayout.widget.ConstraintSet.START
+import androidx.constraintlayout.widget.ConstraintSet.TOP
 import com.android.systemui.R
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.flags.FeatureFlags
@@ -171,33 +175,23 @@
 
     private fun calculateBottomSpacing(): Paddings {
         val containerPadding: Int
-        var stackScrollMargin = notificationsBottomMargin
-        if (splitShadeEnabled) {
-            if (isGestureNavigation) {
-                // only default cutout padding, taskbar always hides
-                containerPadding = bottomCutoutInsets
-            } else if (taskbarVisible) {
-                // navigation buttons + visible taskbar means we're NOT on homescreen
-                containerPadding = bottomStableInsets
-            } else {
-                // navigation buttons + hidden taskbar means we're on homescreen
-                containerPadding = 0
-                // we need extra margin for notifications as navigation buttons are below them
-                stackScrollMargin = bottomStableInsets + notificationsBottomMargin
-            }
+        val stackScrollMargin: Int
+        if (!splitShadeEnabled && (isQSCustomizing || isQSDetailShowing)) {
+            // Clear out bottom paddings/margins so the qs customization can be full height.
+            containerPadding = 0
+            stackScrollMargin = 0
+        } else if (isGestureNavigation) {
+            // only default cutout padding, taskbar always hides
+            containerPadding = bottomCutoutInsets
+            stackScrollMargin = notificationsBottomMargin
+        } else if (taskbarVisible) {
+            // navigation buttons + visible taskbar means we're NOT on homescreen
+            containerPadding = bottomStableInsets
+            stackScrollMargin = notificationsBottomMargin
         } else {
-            if (isQSCustomizing || isQSDetailShowing) {
-                // Clear out bottom paddings/margins so the qs customization can be full height.
-                containerPadding = 0
-                stackScrollMargin = 0
-            } else if (isGestureNavigation) {
-                containerPadding = bottomCutoutInsets
-            } else if (taskbarVisible) {
-                containerPadding = bottomStableInsets
-            } else {
-                containerPadding = 0
-                stackScrollMargin = bottomStableInsets + notificationsBottomMargin
-            }
+            // navigation buttons + hidden taskbar means we're on homescreen
+            containerPadding = 0
+            stackScrollMargin = bottomStableInsets + notificationsBottomMargin
         }
         val qsContainerPadding = if (!(isQSCustomizing || isQSDetailShowing)) {
             // We also want this padding in the bottom in these cases
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 420f21d..14cc6bf 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
@@ -38,6 +38,7 @@
 import android.service.notification.StatusBarNotification;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.Log;
 
 import androidx.annotation.NonNull;
 import androidx.annotation.VisibleForTesting;
@@ -126,6 +127,9 @@
     private List<ListEntry> mReadOnlyNewNotifList = Collections.unmodifiableList(mNewNotifList);
     private final NotifPipelineChoreographer mChoreographer;
 
+    private int mConsecutiveReentrantRebuilds = 0;
+    @VisibleForTesting public static final int MAX_CONSECUTIVE_REENTRANT_REBUILDS = 3;
+
     @Inject
     public ShadeListBuilder(
             DumpManager dumpManager,
@@ -310,7 +314,7 @@
 
                     mLogger.logOnBuildList(reason);
                     mAllEntries = entries;
-                    mChoreographer.schedule();
+                    scheduleRebuild(/* reentrant = */ false);
                 }
             };
 
@@ -1332,11 +1336,64 @@
         throw new RuntimeException("Missing default sectioner!");
     }
 
-    private void rebuildListIfBefore(@PipelineState.StateName int state) {
-        mPipelineState.requireIsBefore(state);
-        if (mPipelineState.is(STATE_IDLE)) {
-            mChoreographer.schedule();
+    private void rebuildListIfBefore(@PipelineState.StateName int rebuildState) {
+        final @PipelineState.StateName int currentState = mPipelineState.getState();
+
+        // If the pipeline is idle, requesting an invalidation is always okay, and starts a new run.
+        if (currentState == STATE_IDLE) {
+            scheduleRebuild(/* reentrant = */ false, rebuildState);
+            return;
         }
+
+        // If the pipeline is running, it is okay to request an invalidation of a *later* stage.
+        // Since the current pipeline run hasn't run it yet, no new pipeline run is needed.
+        if (rebuildState > currentState) {
+            return;
+        }
+
+        // If the pipeline is running, it is bad to request an invalidation of *earlier* stages or
+        // the *current* stage; this will run the pipeline more often than needed, and may even
+        // cause an infinite loop of pipeline runs.
+        //
+        // Unfortunately, there are some unfixed bugs that cause reentrant pipeline runs, so we keep
+        // a counter and allow a few reentrant runs in a row between any two non-reentrant runs.
+        //
+        // It is technically possible for a *pair* of invalidations, one reentrant and one not, to
+        // trigger *each other*, alternating responsibility for pipeline runs in an infinite loop
+        // but constantly resetting the reentrant run counter. Hopefully that doesn't happen.
+        scheduleRebuild(/* reentrant = */ true, rebuildState);
+    }
+
+    private void scheduleRebuild(boolean reentrant) {
+        scheduleRebuild(reentrant, STATE_IDLE);
+    }
+
+    private void scheduleRebuild(boolean reentrant, @PipelineState.StateName int rebuildState) {
+        if (!reentrant) {
+            mConsecutiveReentrantRebuilds = 0;
+            mChoreographer.schedule();
+            return;
+        }
+
+        final @PipelineState.StateName int currentState = mPipelineState.getState();
+
+        final String rebuildStateName = PipelineState.getStateName(rebuildState);
+        final String currentStateName = PipelineState.getStateName(currentState);
+        final IllegalStateException exception = new IllegalStateException(
+                "Reentrant notification pipeline rebuild of state " + rebuildStateName
+                        + " while pipeline in state " + currentStateName + ".");
+
+        mConsecutiveReentrantRebuilds++;
+
+        if (mConsecutiveReentrantRebuilds > MAX_CONSECUTIVE_REENTRANT_REBUILDS) {
+            Log.e(TAG, "Crashing after more than " + MAX_CONSECUTIVE_REENTRANT_REBUILDS
+                    + " consecutive reentrant notification pipeline rebuilds.", exception);
+            throw exception;
+        }
+
+        Log.e(TAG, "Allowing " + mConsecutiveReentrantRebuilds
+                + " consecutive reentrant notification pipeline rebuild(s).", exception);
+        mChoreographer.schedule();
     }
 
     private static int countChildren(List<ListEntry> entries) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
index ef1e57b..6e76691 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java
@@ -286,7 +286,7 @@
                 if (isInflated(child)) {
                     // TODO: May want to put an animation hint here so view manager knows to treat
                     //  this differently from a regular removal animation
-                    freeNotifViews(child);
+                    freeNotifViews(child, "Past last visible group child");
                 }
             }
         }
@@ -379,7 +379,8 @@
         mNotifInflatingFilter.invalidateList("onInflationFinished for " + logKey(entry));
     }
 
-    private void freeNotifViews(NotificationEntry entry) {
+    private void freeNotifViews(NotificationEntry entry, String reason) {
+        mLogger.logFreeNotifViews(entry, reason);
         mViewBarn.removeViewForEntry(entry);
         mNotifInflater.releaseViews(entry);
         // TODO: clear the entry's row here, or even better, stop setting the row on the entry!
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
index 30f1315..c4f4ed5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorLogger.kt
@@ -31,7 +31,7 @@
         buffer.log(TAG, LogLevel.DEBUG, {
             str1 = entry.logKey
         }, {
-            "NOTIF INFLATED $str1"
+            "Inflation completed for notif $str1"
         })
     }
 
@@ -40,7 +40,16 @@
             str1 = entry.logKey
             str2 = reason
         }, {
-            "NOTIF INFLATION ABORTED $str1 reason=$str2"
+            "Infation aborted for notif $str1 reason=$str2"
+        })
+    }
+
+    fun logFreeNotifViews(entry: NotificationEntry, reason: String) {
+        buffer.log(TAG, LogLevel.DEBUG, {
+            str1 = entry.logKey
+            str2 = reason
+        }, {
+            "Freeing content views for notif $str1 reason=$str2"
         })
     }
 
@@ -70,4 +79,4 @@
     }
 }
 
-private const val TAG = "PreparationCoordinator"
\ No newline at end of file
+private const val TAG = "PreparationCoordinator"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
index 96b9aca..2763750 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
@@ -17,11 +17,14 @@
 package com.android.systemui.statusbar.phone
 
 import android.annotation.ColorInt
+import android.app.WallpaperManager
 import android.graphics.Color
+import android.os.Handler
 import android.os.RemoteException
 import android.view.IWindowManager
 import com.android.systemui.Dumpable
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
 import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
@@ -37,6 +40,8 @@
     private val windowManager: IWindowManager,
     @Background private val backgroundExecutor: Executor,
     private val dumpManager: DumpManager,
+    private val wallpaperManager: WallpaperManager,
+    @Main private val mainHandler: Handler,
 ) : CentralSurfacesComponent.Startable, Dumpable {
 
     @ColorInt
@@ -46,9 +51,18 @@
     var isLetterboxBackgroundMultiColored: Boolean = false
         private set
 
+    private val wallpaperColorsListener =
+        WallpaperManager.OnColorsChangedListener { _, _ ->
+            fetchBackgroundColorInfo()
+        }
+
     override fun start() {
         dumpManager.registerDumpable(javaClass.simpleName, this)
+        fetchBackgroundColorInfo()
+        wallpaperManager.addOnColorsChangedListener(wallpaperColorsListener, mainHandler)
+    }
 
+    private fun fetchBackgroundColorInfo() {
         // Using a background executor, as binder calls to IWindowManager are blocking
         backgroundExecutor.execute {
             try {
@@ -62,6 +76,7 @@
 
     override fun stop() {
         dumpManager.unregisterDumpable(javaClass.simpleName)
+        wallpaperManager.removeOnColorsChangedListener(wallpaperColorsListener)
     }
 
     override fun dump(pw: PrintWriter, args: Array<out String>) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
new file mode 100644
index 0000000..a78886f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java
@@ -0,0 +1,96 @@
+/*
+ * 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.dreams;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class DreamOverlayStatusBarItemsProviderTest extends SysuiTestCase {
+    @Mock
+    DreamOverlayStatusBarItemsProvider.Callback mCallback;
+    @Mock
+    DreamOverlayStatusBarItemsProvider.StatusBarItem mStatusBarItem;
+
+    private final Executor mMainExecutor = Runnable::run;
+
+    DreamOverlayStatusBarItemsProvider mProvider;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mProvider = new DreamOverlayStatusBarItemsProvider(mMainExecutor);
+    }
+
+    @Test
+    public void addingCallbackCallsOnStatusBarItemsChanged() {
+        mProvider.addStatusBarItem(mStatusBarItem);
+        mProvider.addCallback(mCallback);
+        verify(mCallback).onStatusBarItemsChanged(List.of(mStatusBarItem));
+    }
+
+    @Test
+    public void addingStatusBarItemCallsOnStatusBarItemsChanged() {
+        mProvider.addCallback(mCallback);
+        mProvider.addStatusBarItem(mStatusBarItem);
+        verify(mCallback).onStatusBarItemsChanged(List.of(mStatusBarItem));
+    }
+
+    @Test
+    public void addingDuplicateStatusBarItemDoesNotCallOnStatusBarItemsChanged() {
+        mProvider.addCallback(mCallback);
+        mProvider.addStatusBarItem(mStatusBarItem);
+        mProvider.addStatusBarItem(mStatusBarItem);
+        // Called only once for addStatusBarItem.
+        verify(mCallback, times(1))
+                .onStatusBarItemsChanged(List.of(mStatusBarItem));
+    }
+
+    @Test
+    public void removingStatusBarItemCallsOnStatusBarItemsChanged() {
+        mProvider.addCallback(mCallback);
+        mProvider.addStatusBarItem(mStatusBarItem);
+        mProvider.removeStatusBarItem(mStatusBarItem);
+        // Called once for addStatusBarItem and once for removeStatusBarItem.
+        verify(mCallback, times(2)).onStatusBarItemsChanged(any());
+    }
+
+    @Test
+    public void removingNonexistentStatusBarItemDoesNotCallOnStatusBarItemsChanged() {
+        mProvider.addCallback(mCallback);
+        mProvider.removeStatusBarItem(mStatusBarItem);
+        verify(mCallback, never()).onStatusBarItemsChanged(any());
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 60e5a94..01309f8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -57,6 +57,7 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.List;
 import java.util.Optional;
 import java.util.concurrent.Executor;
 
@@ -94,6 +95,12 @@
     DreamOverlayNotificationCountProvider mDreamOverlayNotificationCountProvider;
     @Mock
     StatusBarWindowStateController mStatusBarWindowStateController;
+    @Mock
+    DreamOverlayStatusBarItemsProvider mDreamOverlayStatusBarItemsProvider;
+    @Mock
+    DreamOverlayStatusBarItemsProvider.StatusBarItem mStatusBarItem;
+    @Mock
+    View mStatusBarItemView;
 
     private final Executor mMainExecutor = Runnable::run;
 
@@ -118,7 +125,8 @@
                 mSensorPrivacyController,
                 Optional.of(mDreamOverlayNotificationCountProvider),
                 mZenModeController,
-                mStatusBarWindowStateController);
+                mStatusBarWindowStateController,
+                mDreamOverlayStatusBarItemsProvider);
     }
 
     @Test
@@ -128,6 +136,7 @@
         verify(mSensorPrivacyController).addCallback(any());
         verify(mZenModeController).addCallback(any());
         verify(mDreamOverlayNotificationCountProvider).addCallback(any());
+        verify(mDreamOverlayStatusBarItemsProvider).addCallback(any());
     }
 
     @Test
@@ -256,7 +265,8 @@
                 mSensorPrivacyController,
                 Optional.empty(),
                 mZenModeController,
-                mStatusBarWindowStateController);
+                mStatusBarWindowStateController,
+                mDreamOverlayStatusBarItemsProvider);
         controller.onViewAttached();
         verify(mView, never()).showIcon(
                 eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any());
@@ -294,6 +304,7 @@
         verify(mSensorPrivacyController).removeCallback(any());
         verify(mZenModeController).removeCallback(any());
         verify(mDreamOverlayNotificationCountProvider).removeCallback(any());
+        verify(mDreamOverlayStatusBarItemsProvider).removeCallback(any());
     }
 
     @Test
@@ -462,4 +473,18 @@
 
         verify(mView, never()).setVisibility(anyInt());
     }
+
+    @Test
+    public void testExtraStatusBarItemSetWhenItemsChange() {
+        mController.onViewAttached();
+        when(mStatusBarItem.getView()).thenReturn(mStatusBarItemView);
+
+        final ArgumentCaptor<DreamOverlayStatusBarItemsProvider.Callback>
+                callbackCapture = ArgumentCaptor.forClass(
+                        DreamOverlayStatusBarItemsProvider.Callback.class);
+        verify(mDreamOverlayStatusBarItemsProvider).addCallback(callbackCapture.capture());
+        callbackCapture.getValue().onStatusBarItemsChanged(List.of(mStatusBarItem));
+
+        verify(mView).setExtraStatusBarItemViews(List.of(mStatusBarItemView));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
index 642e29b..2ba8782 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt
@@ -22,13 +22,17 @@
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.phone.MultiUserSwitchController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
+import com.android.systemui.statusbar.policy.FakeConfigurationController
 import com.android.systemui.statusbar.policy.UserInfoController
 import com.android.systemui.util.mockito.capture
 import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.utils.leaks.LeakCheckedTest
+import com.google.common.truth.Expect
 import com.google.common.truth.Truth.assertThat
+import javax.inject.Provider
 import org.junit.After
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.ArgumentCaptor
@@ -42,47 +46,38 @@
 import org.mockito.Mockito.never
 import org.mockito.Mockito.reset
 import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import javax.inject.Provider
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 @SmallTest
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @RunWith(AndroidTestingRunner::class)
 class FooterActionsControllerTest : LeakCheckedTest() {
-    @Mock
-    private lateinit var userManager: UserManager
-    @Mock
-    private lateinit var userTracker: UserTracker
-    @Mock
-    private lateinit var activityStarter: ActivityStarter
-    @Mock
-    private lateinit var deviceProvisionedController: DeviceProvisionedController
-    @Mock
-    private lateinit var userInfoController: UserInfoController
-    @Mock
-    private lateinit var multiUserSwitchControllerFactory: MultiUserSwitchController.Factory
-    @Mock
-    private lateinit var multiUserSwitchController: MultiUserSwitchController
-    @Mock
-    private lateinit var globalActionsDialogProvider: Provider<GlobalActionsDialogLite>
-    @Mock
-    private lateinit var globalActionsDialog: GlobalActionsDialogLite
-    @Mock
-    private lateinit var uiEventLogger: UiEventLogger
-    @Mock
-    private lateinit var securityFooterController: QSSecurityFooter
-    @Mock
-    private lateinit var fgsManagerController: QSFgsManagerFooter
+
+    @get:Rule var expect: Expect = Expect.create()
+
+    @Mock private lateinit var userManager: UserManager
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
+    @Mock private lateinit var userInfoController: UserInfoController
+    @Mock private lateinit var multiUserSwitchControllerFactory: MultiUserSwitchController.Factory
+    @Mock private lateinit var multiUserSwitchController: MultiUserSwitchController
+    @Mock private lateinit var globalActionsDialogProvider: Provider<GlobalActionsDialogLite>
+    @Mock private lateinit var globalActionsDialog: GlobalActionsDialogLite
+    @Mock private lateinit var uiEventLogger: UiEventLogger
+    @Mock private lateinit var securityFooterController: QSSecurityFooter
+    @Mock private lateinit var fgsManagerController: QSFgsManagerFooter
     @Captor
     private lateinit var visibilityChangedCaptor:
         ArgumentCaptor<VisibilityChangedDispatcher.OnVisibilityChangedListener>
 
     private lateinit var controller: FooterActionsController
 
+    private val configurationController = FakeConfigurationController()
     private val metricsLogger: MetricsLogger = FakeMetricsLogger()
-    private lateinit var view: FooterActionsView
     private val falsingManager: FalsingManagerFake = FalsingManagerFake()
+    private lateinit var view: FooterActionsView
     private lateinit var testableLooper: TestableLooper
     private lateinit var fakeSettings: FakeSettings
     private lateinit var securityFooter: View
@@ -90,12 +85,15 @@
 
     @Before
     fun setUp() {
+        // We want to make sure testable resources are always used
+        context.ensureTestableResources()
+
         MockitoAnnotations.initMocks(this)
         testableLooper = TestableLooper.get(this)
         fakeSettings = FakeSettings()
 
         whenever(multiUserSwitchControllerFactory.create(any()))
-                .thenReturn(multiUserSwitchController)
+            .thenReturn(multiUserSwitchController)
         whenever(globalActionsDialogProvider.get()).thenReturn(globalActionsDialog)
 
         securityFooter = View(mContext)
@@ -135,7 +133,7 @@
         view.findViewById<View>(R.id.pm_lite).performClick()
         // Verify clicks are logged
         verify(uiEventLogger, Mockito.times(1))
-                .log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
+            .log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS)
     }
 
     @Test
@@ -299,6 +297,86 @@
         assertThat(booleanCaptor.allValues.last()).isTrue()
     }
 
+    @Test
+    fun setExpansion_inSplitShade_alphaFollowsExpansion() {
+        enableSplitShade()
+
+        controller.setExpansion(0f)
+        expect.that(view.alpha).isEqualTo(0f)
+
+        controller.setExpansion(0.25f)
+        expect.that(view.alpha).isEqualTo(0.25f)
+
+        controller.setExpansion(0.5f)
+        expect.that(view.alpha).isEqualTo(0.5f)
+
+        controller.setExpansion(0.75f)
+        expect.that(view.alpha).isEqualTo(0.75f)
+
+        controller.setExpansion(1f)
+        expect.that(view.alpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun setExpansion_inSplitShade_backgroundAlphaFollowsExpansion_with_0_9_delay() {
+        enableSplitShade()
+
+        controller.setExpansion(0f)
+        expect.that(view.backgroundAlphaFraction).isEqualTo(0f)
+
+        controller.setExpansion(0.5f)
+        expect.that(view.backgroundAlphaFraction).isEqualTo(0f)
+
+        controller.setExpansion(0.9f)
+        expect.that(view.backgroundAlphaFraction).isEqualTo(0f)
+
+        controller.setExpansion(0.91f)
+        expect.that(view.backgroundAlphaFraction).isWithin(FLOAT_TOLERANCE).of(0.1f)
+
+        controller.setExpansion(0.95f)
+        expect.that(view.backgroundAlphaFraction).isWithin(FLOAT_TOLERANCE).of(0.5f)
+
+        controller.setExpansion(1f)
+        expect.that(view.backgroundAlphaFraction).isEqualTo(1f)
+    }
+
+    @Test
+    fun setExpansion_inSingleShade_alphaFollowsExpansion_with_0_9_delay() {
+        disableSplitShade()
+
+        controller.setExpansion(0f)
+        expect.that(view.alpha).isEqualTo(0f)
+
+        controller.setExpansion(0.5f)
+        expect.that(view.alpha).isEqualTo(0f)
+
+        controller.setExpansion(0.9f)
+        expect.that(view.alpha).isEqualTo(0f)
+
+        controller.setExpansion(0.91f)
+        expect.that(view.alpha).isWithin(FLOAT_TOLERANCE).of(0.1f)
+
+        controller.setExpansion(0.95f)
+        expect.that(view.alpha).isWithin(FLOAT_TOLERANCE).of(0.5f)
+
+        controller.setExpansion(1f)
+        expect.that(view.alpha).isEqualTo(1f)
+    }
+
+    @Test
+    fun setExpansion_inSingleShade_backgroundAlphaAlways1() {
+        disableSplitShade()
+
+        controller.setExpansion(0f)
+        expect.that(view.backgroundAlphaFraction).isEqualTo(1f)
+
+        controller.setExpansion(0.5f)
+        expect.that(view.backgroundAlphaFraction).isEqualTo(1f)
+
+        controller.setExpansion(1f)
+        expect.that(view.backgroundAlphaFraction).isEqualTo(1f)
+    }
+
     private fun setVisibilities(
         securityFooterVisible: Boolean,
         fgsFooterVisible: Boolean,
@@ -311,15 +389,52 @@
     }
 
     private fun inflateView(): FooterActionsView {
-        return LayoutInflater.from(context)
-                .inflate(R.layout.footer_actions, null) as FooterActionsView
+        return LayoutInflater.from(context).inflate(R.layout.footer_actions, null)
+            as FooterActionsView
     }
 
     private fun constructFooterActionsController(view: FooterActionsView): FooterActionsController {
-        return FooterActionsController(view, multiUserSwitchControllerFactory,
-                activityStarter, userManager, userTracker, userInfoController,
-                deviceProvisionedController, securityFooterController, fgsManagerController,
-                falsingManager, metricsLogger, globalActionsDialogProvider, uiEventLogger,
-                showPMLiteButton = true, fakeSettings, Handler(testableLooper.looper))
+        return FooterActionsController(
+            view,
+            multiUserSwitchControllerFactory,
+            activityStarter,
+            userManager,
+            userTracker,
+            userInfoController,
+            deviceProvisionedController,
+            securityFooterController,
+            fgsManagerController,
+            falsingManager,
+            metricsLogger,
+            globalActionsDialogProvider,
+            uiEventLogger,
+            showPMLiteButton = true,
+            fakeSettings,
+            Handler(testableLooper.looper),
+            configurationController)
     }
-}
\ No newline at end of file
+
+    private fun enableSplitShade() {
+        setSplitShadeEnabled(true)
+    }
+
+    private fun disableSplitShade() {
+        setSplitShadeEnabled(false)
+    }
+
+    private fun setSplitShadeEnabled(enabled: Boolean) {
+        overrideResource(R.bool.config_use_split_notification_shade, enabled)
+        configurationController.notifyConfigurationChanged()
+    }
+}
+
+private const val FLOAT_TOLERANCE = 0.01f
+
+private val View.backgroundAlphaFraction: Float?
+    get() {
+        return if (background != null) {
+            background.alpha / 255f
+        } else {
+            null
+        }
+    }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
index 32c66d2..10f6ce8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -205,6 +205,40 @@
     }
 
     @Test
+    public void setQsExpansion_inSplitShade_setsFooterActionsExpansion_basedOnPanelExpFraction() {
+        // Random test values without any meaning. They just have to be different from each other.
+        float expansion = 0.123f;
+        float panelExpansionFraction = 0.321f;
+        float proposedTranslation = 456f;
+        float squishinessFraction = 0.987f;
+
+        QSFragment fragment = resumeAndGetFragment();
+        enableSplitShade();
+
+        fragment.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
+                squishinessFraction);
+
+        verify(mQSFooterActionController).setExpansion(panelExpansionFraction);
+    }
+
+    @Test
+    public void setQsExpansion_notInSplitShade_setsFooterActionsExpansion_basedOnExpansion() {
+        // Random test values without any meaning. They just have to be different from each other.
+        float expansion = 0.123f;
+        float panelExpansionFraction = 0.321f;
+        float proposedTranslation = 456f;
+        float squishinessFraction = 0.987f;
+
+        QSFragment fragment = resumeAndGetFragment();
+        disableSplitShade();
+
+        fragment.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation,
+                squishinessFraction);
+
+        verify(mQSFooterActionController).setExpansion(expansion);
+    }
+
+    @Test
     public void getQsMinExpansionHeight_notInSplitShade_returnsHeaderHeight() {
         QSFragment fragment = resumeAndGetFragment();
         disableSplitShade();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
index 360eef9..cf5fa87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/system/RemoteTransitionTest.java
@@ -19,12 +19,14 @@
 import static android.view.WindowManager.TRANSIT_CHANGE;
 import static android.view.WindowManager.TRANSIT_CLOSE;
 import static android.view.WindowManager.TRANSIT_OPEN;
+import static android.window.TransitionInfo.FLAG_FIRST_CUSTOM;
 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_HOME;
+import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.ACTIVITY_TYPE_STANDARD;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CHANGING;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_CLOSING;
 import static com.android.systemui.shared.system.RemoteAnimationTargetCompat.MODE_OPENING;
@@ -64,12 +66,15 @@
     @Test
     public void testLegacyTargetExtract() {
         TransitionInfo combined = new TransitionInfoBuilder(TRANSIT_CLOSE)
-                .addChange(TRANSIT_CHANGE, FLAG_SHOW_WALLPAPER)
-                .addChange(TRANSIT_CLOSE, 0 /* flags */)
-                .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER).build();
-        // Check non-wallpaper extraction
-        RemoteAnimationTargetCompat[] wrapped = RemoteAnimationTargetCompat.wrap(combined,
-                false /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
+                .addChange(TRANSIT_CHANGE, FLAG_SHOW_WALLPAPER,
+                        createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_STANDARD))
+                .addChange(TRANSIT_CLOSE, 0 /* flags */,
+                        createTaskInfo(2 /* taskId */, ACTIVITY_TYPE_STANDARD))
+                .addChange(TRANSIT_OPEN, FLAG_IS_WALLPAPER, null /* taskInfo */)
+                .addChange(TRANSIT_CHANGE, FLAG_FIRST_CUSTOM, null /* taskInfo */).build();
+        // Check apps extraction
+        RemoteAnimationTargetCompat[] wrapped = RemoteAnimationTargetCompat.wrapApps(combined,
+                mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(2, wrapped.length);
         int changeLayer = -1;
         int closeLayer = -1;
@@ -86,17 +91,25 @@
         assertTrue(closeLayer < changeLayer);
 
         // Check wallpaper extraction
-        RemoteAnimationTargetCompat[] wallps = RemoteAnimationTargetCompat.wrap(combined,
+        RemoteAnimationTargetCompat[] wallps = RemoteAnimationTargetCompat.wrapNonApps(combined,
                 true /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
         assertEquals(1, wallps.length);
         assertTrue(wallps[0].prefixOrderIndex < closeLayer);
         assertEquals(MODE_OPENING, wallps[0].mode);
+
+        // Check non-apps extraction
+        RemoteAnimationTargetCompat[] nonApps = RemoteAnimationTargetCompat.wrapNonApps(combined,
+                false /* wallpapers */, mock(SurfaceControl.Transaction.class), null /* leashes */);
+        assertEquals(1, nonApps.length);
+        assertTrue(nonApps[0].prefixOrderIndex < closeLayer);
+        assertEquals(MODE_CHANGING, nonApps[0].mode);
     }
 
     @Test
     public void testLegacyTargetWrapper() {
         TransitionInfo tinfo = new TransitionInfoBuilder(TRANSIT_CLOSE)
-                .addChange(TRANSIT_CHANGE, FLAG_TRANSLUCENT).build();
+                .addChange(TRANSIT_CHANGE, FLAG_TRANSLUCENT,
+                        createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_STANDARD)).build();
         final TransitionInfo.Change change = tinfo.getChanges().get(0);
         final Rect endBounds = new Rect(40, 60, 140, 200);
         change.setTaskInfo(createTaskInfo(1 /* taskId */, ACTIVITY_TYPE_HOME));
@@ -119,11 +132,12 @@
         }
 
         TransitionInfoBuilder addChange(@WindowManager.TransitionType int mode,
-                @TransitionInfo.ChangeFlags int flags) {
+                @TransitionInfo.ChangeFlags int flags, ActivityManager.RunningTaskInfo taskInfo) {
             final TransitionInfo.Change change =
                     new TransitionInfo.Change(null /* token */, createMockSurface(true));
             change.setMode(mode);
             change.setFlags(flags);
+            change.setTaskInfo(taskInfo);
             mInfo.addChange(change);
             return this;
         }
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 dfa38ab..9f21409 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
@@ -68,6 +68,7 @@
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
+import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CollectionReadyForBuildListener;
 import com.android.systemui.util.time.FakeSystemClock;
 
@@ -1715,66 +1716,201 @@
         assertEquals(GroupEntry.ROOT_ENTRY, group.getPreviousParent());
     }
 
-    @Test(expected = IllegalStateException.class)
-    public void testOutOfOrderPreGroupFilterInvalidationThrows() {
-        // GIVEN a PreGroupNotifFilter that gets invalidated during the grouping stage
-        NotifFilter filter = new PackageFilter(PACKAGE_5);
-        OnBeforeTransformGroupsListener listener = (list) -> filter.invalidateList(null);
+    static class CountingInvalidator {
+        CountingInvalidator(Pluggable pluggableToInvalidate) {
+            mPluggableToInvalidate = pluggableToInvalidate;
+            mInvalidationCount = 0;
+        }
+
+        public void setInvalidationCount(int invalidationCount) {
+            mInvalidationCount = invalidationCount;
+        }
+
+        public void maybeInvalidate() {
+            if (mInvalidationCount > 0) {
+                mPluggableToInvalidate.invalidateList("test invalidation");
+                mInvalidationCount--;
+            }
+        }
+
+        private Pluggable mPluggableToInvalidate;
+        private int mInvalidationCount;
+
+        private static final String TAG = "ShadeListBuilderTestCountingInvalidator";
+    }
+
+    @Test
+    public void testOutOfOrderPreGroupFilterInvalidationDoesNotThrowBeforeTooManyRuns() {
+        // GIVEN a PreGroupNotifFilter that gets invalidated during the grouping stage,
+        NotifFilter filter = new PackageFilter(PACKAGE_1);
+        CountingInvalidator invalidator = new CountingInvalidator(filter);
+        OnBeforeTransformGroupsListener listener = (list) -> invalidator.maybeInvalidate();
         mListBuilder.addPreGroupFilter(filter);
         mListBuilder.addOnBeforeTransformGroupsListener(listener);
 
-        // WHEN we try to run the pipeline and the filter is invalidated
-        addNotif(0, PACKAGE_1);
+        // WHEN we try to run the pipeline and the filter is invalidated exactly
+        // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
+        addNotif(0, PACKAGE_2);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS);
         dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
 
-        // THEN an exception is thrown
+        // THEN an exception is NOT thrown.
     }
 
     @Test(expected = IllegalStateException.class)
-    public void testOutOfOrderPrompterInvalidationThrows() {
-        // GIVEN a NotifPromoter that gets invalidated during the sorting stage
+    public void testOutOfOrderPreGroupFilterInvalidationThrowsAfterTooManyRuns() {
+        // GIVEN a PreGroupNotifFilter that gets invalidated during the grouping stage,
+        NotifFilter filter = new PackageFilter(PACKAGE_1);
+        CountingInvalidator invalidator = new CountingInvalidator(filter);
+        OnBeforeTransformGroupsListener listener = (list) -> invalidator.maybeInvalidate();
+        mListBuilder.addPreGroupFilter(filter);
+        mListBuilder.addOnBeforeTransformGroupsListener(listener);
+
+        // WHEN we try to run the pipeline and the filter is invalidated more than
+        // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
+        addNotif(0, PACKAGE_2);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
+        dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
+
+        // THEN an exception IS thrown.
+    }
+
+    @Test
+    public void testNonConsecutiveOutOfOrderInvalidationDontThrowAfterTooManyRuns() {
+        // GIVEN a PreGroupNotifFilter that gets invalidated during the grouping stage,
+        NotifFilter filter = new PackageFilter(PACKAGE_1);
+        CountingInvalidator invalidator = new CountingInvalidator(filter);
+        OnBeforeTransformGroupsListener listener = (list) -> invalidator.maybeInvalidate();
+        mListBuilder.addPreGroupFilter(filter);
+        mListBuilder.addOnBeforeTransformGroupsListener(listener);
+
+        // WHEN we try to run the pipeline and the filter is invalidated at least
+        // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
+        addNotif(0, PACKAGE_2);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS);
+        dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS);
+        dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
+
+        // THEN an exception is NOT thrown.
+    }
+
+    @Test
+    public void testOutOfOrderPrompterInvalidationDoesNotThrowBeforeTooManyRuns() {
+        // GIVEN a NotifPromoter that gets invalidated during the sorting stage,
         NotifPromoter promoter = new IdPromoter(47);
-        OnBeforeSortListener listener =
-                (list) -> promoter.invalidateList(null);
+        CountingInvalidator invalidator = new CountingInvalidator(promoter);
+        OnBeforeSortListener listener = (list) -> invalidator.maybeInvalidate();
         mListBuilder.addPromoter(promoter);
         mListBuilder.addOnBeforeSortListener(listener);
 
-        // WHEN we try to run the pipeline and the promoter is invalidated
+        // WHEN we try to run the pipeline and the promoter is invalidated exactly
+        // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
         addNotif(0, PACKAGE_1);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS);
         dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
 
-        // THEN an exception is thrown
+        // THEN an exception is NOT thrown.
     }
 
     @Test(expected = IllegalStateException.class)
-    public void testOutOfOrderComparatorInvalidationThrows() {
-        // GIVEN a NotifComparator that gets invalidated during the finalizing stage
-        NotifComparator comparator = new HypeComparator(PACKAGE_5);
-        OnBeforeRenderListListener listener =
-                (list) -> comparator.invalidateList(null);
+    public void testOutOfOrderPrompterInvalidationThrowsAfterTooManyRuns() {
+        // GIVEN a NotifPromoter that gets invalidated during the sorting stage,
+        NotifPromoter promoter = new IdPromoter(47);
+        CountingInvalidator invalidator = new CountingInvalidator(promoter);
+        OnBeforeSortListener listener = (list) -> invalidator.maybeInvalidate();
+        mListBuilder.addPromoter(promoter);
+        mListBuilder.addOnBeforeSortListener(listener);
+
+        // WHEN we try to run the pipeline and the promoter is invalidated more than
+        // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
+        addNotif(0, PACKAGE_1);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
+        dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
+
+        // THEN an exception IS thrown.
+    }
+
+    @Test
+    public void testOutOfOrderComparatorInvalidationDoesNotThrowBeforeTooManyRuns() {
+        // GIVEN a NotifComparator that gets invalidated during the finalizing stage,
+        NotifComparator comparator = new HypeComparator(PACKAGE_1);
+        CountingInvalidator invalidator = new CountingInvalidator(comparator);
+        OnBeforeRenderListListener listener = (list) -> invalidator.maybeInvalidate();
         mListBuilder.setComparators(singletonList(comparator));
         mListBuilder.addOnBeforeRenderListListener(listener);
 
-        // WHEN we try to run the pipeline and the comparator is invalidated
-        addNotif(0, PACKAGE_1);
+        // WHEN we try to run the pipeline and the comparator is invalidated exactly
+        // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
+        addNotif(0, PACKAGE_2);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS);
         dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
 
-        // THEN an exception is thrown
+        // THEN an exception is NOT thrown.
     }
 
     @Test(expected = IllegalStateException.class)
-    public void testOutOfOrderPreRenderFilterInvalidationThrows() {
-        // GIVEN a PreRenderNotifFilter that gets invalidated during the finalizing stage
-        NotifFilter filter = new PackageFilter(PACKAGE_5);
-        OnBeforeRenderListListener listener = (list) -> filter.invalidateList(null);
+    public void testOutOfOrderComparatorInvalidationThrowsAfterTooManyRuns() {
+        // GIVEN a NotifComparator that gets invalidated during the finalizing stage,
+        NotifComparator comparator = new HypeComparator(PACKAGE_1);
+        CountingInvalidator invalidator = new CountingInvalidator(comparator);
+        OnBeforeRenderListListener listener = (list) -> invalidator.maybeInvalidate();
+        mListBuilder.setComparators(singletonList(comparator));
+        mListBuilder.addOnBeforeRenderListListener(listener);
+
+        // WHEN we try to run the pipeline and the comparator is invalidated more than
+        // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
+        addNotif(0, PACKAGE_2);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
+        dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
+
+        // THEN an exception IS thrown.
+    }
+
+    @Test
+    public void testOutOfOrderPreRenderFilterInvalidationDoesNotThrowBeforeTooManyRuns() {
+        // GIVEN a PreRenderNotifFilter that gets invalidated during the finalizing stage,
+        NotifFilter filter = new PackageFilter(PACKAGE_1);
+        CountingInvalidator invalidator = new CountingInvalidator(filter);
+        OnBeforeRenderListListener listener = (list) -> invalidator.maybeInvalidate();
         mListBuilder.addFinalizeFilter(filter);
         mListBuilder.addOnBeforeRenderListListener(listener);
 
-        // WHEN we try to run the pipeline and the PreRenderFilter is invalidated
-        addNotif(0, PACKAGE_1);
+        // WHEN we try to run the pipeline and the PreRenderFilter is invalidated exactly
+        // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
+        addNotif(0, PACKAGE_2);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS);
         dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
 
-        // THEN an exception is thrown
+        // THEN an exception is NOT thrown.
+    }
+
+    @Test(expected = IllegalStateException.class)
+    public void testOutOfOrderPreRenderFilterInvalidationThrowsAfterTooManyRuns() {
+        // GIVEN a PreRenderNotifFilter that gets invalidated during the finalizing stage,
+        NotifFilter filter = new PackageFilter(PACKAGE_1);
+        CountingInvalidator invalidator = new CountingInvalidator(filter);
+        OnBeforeRenderListListener listener = (list) -> invalidator.maybeInvalidate();
+        mListBuilder.addFinalizeFilter(filter);
+        mListBuilder.addOnBeforeRenderListListener(listener);
+
+        // WHEN we try to run the pipeline and the PreRenderFilter is invalidated more than
+        // MAX_CONSECUTIVE_REENTRANT_REBUILDS times,
+        addNotif(0, PACKAGE_2);
+        invalidator.setInvalidationCount(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 1);
+        dispatchBuild();
+        runWhileScheduledUpTo(ShadeListBuilder.MAX_CONSECUTIVE_REENTRANT_REBUILDS + 2);
+
+        // THEN an exception IS thrown.
     }
 
     @Test
@@ -2096,6 +2232,18 @@
         mPipelineChoreographer.runIfScheduled();
     }
 
+    private void runWhileScheduledUpTo(int maxRuns) {
+        int runs = 0;
+        while (mPipelineChoreographer.isScheduled()) {
+            if (runs > maxRuns) {
+                throw new IndexOutOfBoundsException(
+                        "Pipeline scheduled itself more than " + maxRuns + "times");
+            }
+            runs++;
+            mPipelineChoreographer.runIfScheduled();
+        }
+    }
+
     private void verifyBuiltList(ExpectedEntry ...expectedEntries) {
         try {
             assertEquals(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
index 44325dd..a2828d33 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
@@ -16,19 +16,28 @@
 
 package com.android.systemui.statusbar.phone
 
+import android.app.WallpaperManager
+import android.app.WallpaperManager.OnColorsChangedListener
 import android.graphics.Color
+import android.os.Handler
+import android.os.Looper
 import android.testing.AndroidTestingRunner
 import android.view.IWindowManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dump.DumpManager
 import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Expect
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
+import org.junit.Rule
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
+import org.mockito.Mockito.any
+import org.mockito.Mockito.doAnswer
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
@@ -38,17 +47,41 @@
 
     private val fakeSystemClock = FakeSystemClock()
     private val fakeExecutor = FakeExecutor(fakeSystemClock)
+    private val mainHandler = Handler(Looper.getMainLooper())
+
+    @get:Rule var expect: Expect = Expect.create()
 
     @Mock private lateinit var windowManager: IWindowManager
     @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var wallpaperManager: WallpaperManager
 
     private lateinit var provider: LetterboxBackgroundProvider
 
+    private var wallpaperColorsListener: OnColorsChangedListener? = null
+
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
-        provider = LetterboxBackgroundProvider(windowManager, fakeExecutor, dumpManager)
+        setUpWallpaperManager()
+        provider =
+            LetterboxBackgroundProvider(
+                windowManager, fakeExecutor, dumpManager, wallpaperManager, mainHandler)
+    }
+
+    private fun setUpWallpaperManager() {
+        doAnswer { invocation ->
+                wallpaperColorsListener = invocation.arguments[0] as OnColorsChangedListener
+                return@doAnswer Unit
+            }
+            .`when`(wallpaperManager)
+            .addOnColorsChangedListener(any(), eq(mainHandler))
+        doAnswer {
+                wallpaperColorsListener = null
+                return@doAnswer Unit
+            }
+            .`when`(wallpaperManager)
+            .removeOnColorsChangedListener(any(OnColorsChangedListener::class.java))
     }
 
     @Test
@@ -76,6 +109,31 @@
     }
 
     @Test
+    fun letterboxBackgroundColor_returnsValueFromWindowManagerOnlyOnce() {
+        whenever(windowManager.letterboxBackgroundColorInArgb).thenReturn(Color.RED)
+        provider.start()
+        fakeExecutor.runAllReady()
+        expect.that(provider.letterboxBackgroundColor).isEqualTo(Color.RED)
+
+        whenever(windowManager.letterboxBackgroundColorInArgb).thenReturn(Color.GREEN)
+        fakeExecutor.runAllReady()
+        expect.that(provider.letterboxBackgroundColor).isEqualTo(Color.RED)
+    }
+
+    @Test
+    fun letterboxBackgroundColor_afterWallpaperChanges_returnsUpdatedColor() {
+        whenever(windowManager.letterboxBackgroundColorInArgb).thenReturn(Color.RED)
+        provider.start()
+        fakeExecutor.runAllReady()
+
+        whenever(windowManager.letterboxBackgroundColorInArgb).thenReturn(Color.GREEN)
+        wallpaperColorsListener!!.onColorsChanged(null, 0)
+        fakeExecutor.runAllReady()
+
+        assertThat(provider.letterboxBackgroundColor).isEqualTo(Color.GREEN)
+    }
+
+    @Test
     fun isLetterboxBackgroundMultiColored_defaultValue_returnsFalse() {
         assertThat(provider.isLetterboxBackgroundMultiColored).isEqualTo(false)
     }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 74ee680..4f3fd64 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -66,6 +66,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Locale;
 
 import javax.xml.datatype.DatatypeConfigurationException;
 
@@ -708,8 +709,8 @@
     private static DisplayDeviceConfig getConfigFromSuffix(Context context, File baseDirectory,
             String suffixFormat, long idNumber) {
 
-        final String suffix = String.format(suffixFormat, idNumber);
-        final String filename = String.format(CONFIG_FILE_FORMAT, suffix);
+        final String suffix = String.format(Locale.ROOT, suffixFormat, idNumber);
+        final String filename = String.format(Locale.ROOT, CONFIG_FILE_FORMAT, suffix);
         final File filePath = Environment.buildPath(
                 baseDirectory, ETC_DIR, DISPLAY_CONFIG_DIR, filename);
         final DisplayDeviceConfig config = new DisplayDeviceConfig(context);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 75ee447..9c86076 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1315,9 +1315,6 @@
         }
 
         final boolean autoBrightnessAdjustmentChanged = updateAutoBrightnessAdjustment();
-        if (autoBrightnessAdjustmentChanged) {
-            mTemporaryAutoBrightnessAdjustment = Float.NaN;
-        }
 
         // Use the autobrightness adjustment override if set.
         final float autoBrightnessAdjustment;
@@ -2293,14 +2290,15 @@
 
     private void handleSettingsChange(boolean userSwitch) {
         mPendingScreenBrightnessSetting = getScreenBrightnessSetting();
+        mPendingAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
         if (userSwitch) {
             // Don't treat user switches as user initiated change.
             setCurrentScreenBrightness(mPendingScreenBrightnessSetting);
+            updateAutoBrightnessAdjustment();
             if (mAutomaticBrightnessController != null) {
                 mAutomaticBrightnessController.resetShortTermModel();
             }
         }
-        mPendingAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
         // We don't bother with a pending variable for VR screen brightness since we just
         // immediately adapt to it.
         mScreenBrightnessForVr = getScreenBrightnessForVrSetting();
@@ -2369,6 +2367,7 @@
         }
         mAutoBrightnessAdjustment = mPendingAutoBrightnessAdjustment;
         mPendingAutoBrightnessAdjustment = Float.NaN;
+        mTemporaryAutoBrightnessAdjustment = Float.NaN;
         return true;
     }
 
diff --git a/services/core/java/com/android/server/policy/SideFpsEventHandler.java b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
index af2b902..8582f54 100644
--- a/services/core/java/com/android/server/policy/SideFpsEventHandler.java
+++ b/services/core/java/com/android/server/policy/SideFpsEventHandler.java
@@ -23,11 +23,8 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
-import android.app.AlertDialog;
-import android.app.Dialog;
 import android.content.BroadcastReceiver;
 import android.content.Context;
-import android.content.DialogInterface;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.pm.PackageManager;
@@ -35,12 +32,11 @@
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.hardware.fingerprint.IFingerprintAuthenticatorsRegisteredCallback;
-import android.os.Build;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.os.UserHandle;
-import android.provider.Settings;
 import android.util.Log;
+import android.view.View;
+import android.view.Window;
 import android.view.WindowManager;
 
 import com.android.internal.R;
@@ -48,49 +44,48 @@
 
 import java.util.List;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Supplier;
 
 /**
  * Defines behavior for handling interactions between power button events and fingerprint-related
  * operations, for devices where the fingerprint sensor (side fps) lives on the power button.
  */
-public class SideFpsEventHandler {
+public class SideFpsEventHandler implements View.OnClickListener {
 
     private static final int DEBOUNCE_DELAY_MILLIS = 500;
 
-    private int getTapWaitForPowerDuration(Context context) {
-        int tap = context.getResources().getInteger(
-                R.integer.config_sidefpsEnrollPowerPressWindow);
-        if (Build.isDebuggable()) {
-            tap = Settings.Secure.getIntForUser(context.getContentResolver(),
-                    Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW, tap,
-                    UserHandle.USER_CURRENT);
-        }
-        return tap;
-    }
-
     private static final String TAG = "SideFpsEventHandler";
 
-    @NonNull private final Context mContext;
-    @NonNull private final Handler mHandler;
-    @NonNull private final PowerManager mPowerManager;
-    @NonNull private final Supplier<AlertDialog.Builder> mDialogSupplier;
-    @NonNull private final AtomicBoolean mSideFpsEventHandlerReady;
-
-    @Nullable private Dialog mDialog;
+    @NonNull
+    private final Context mContext;
+    @NonNull
+    private final Handler mHandler;
+    @NonNull
+    private final PowerManager mPowerManager;
+    @NonNull
+    private final AtomicBoolean mSideFpsEventHandlerReady;
+    private final int mDismissDialogTimeout;
+    @Nullable
+    private SideFpsToast mDialog;
     private final Runnable mTurnOffDialog =
             () -> {
                 dismissDialog("mTurnOffDialog");
             };
-
-    @NonNull private final DialogInterface.OnDismissListener mDialogDismissListener;
-
     private @BiometricStateListener.State int mBiometricState;
-    private final int mTapWaitForPowerDuration;
     private FingerprintManager mFingerprintManager;
+    private DialogProvider mDialogProvider;
+    private long mLastPowerPressTime;
 
-    SideFpsEventHandler(Context context, Handler handler, PowerManager powerManager) {
-        this(context, handler, powerManager, () -> new AlertDialog.Builder(context));
+    SideFpsEventHandler(
+            Context context,
+            Handler handler,
+            PowerManager powerManager) {
+        this(context, handler, powerManager, (ctx) -> {
+            SideFpsToast dialog = new SideFpsToast(ctx);
+            dialog.getWindow()
+                    .setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
+            dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+            return dialog;
+        });
     }
 
     @VisibleForTesting
@@ -98,23 +93,13 @@
             Context context,
             Handler handler,
             PowerManager powerManager,
-            Supplier<AlertDialog.Builder> dialogSupplier) {
+            DialogProvider provider) {
         mContext = context;
         mHandler = handler;
         mPowerManager = powerManager;
-        mDialogSupplier = dialogSupplier;
         mBiometricState = STATE_IDLE;
         mSideFpsEventHandlerReady = new AtomicBoolean(false);
-        mDialogDismissListener =
-                (dialog) -> {
-                    if (mDialog == dialog) {
-                        if (mHandler != null) {
-                            mHandler.removeCallbacks(mTurnOffDialog);
-                        }
-                        mDialog = null;
-                    }
-                };
-
+        mDialogProvider = provider;
         // ensure dialog is dismissed if screen goes off for unrelated reasons
         context.registerReceiver(
                 new BroadcastReceiver() {
@@ -127,7 +112,13 @@
                     }
                 },
                 new IntentFilter(Intent.ACTION_SCREEN_OFF));
-        mTapWaitForPowerDuration = getTapWaitForPowerDuration(context);
+        mDismissDialogTimeout = context.getResources().getInteger(
+                R.integer.config_sideFpsToastTimeout);
+    }
+
+    @Override
+    public void onClick(View v) {
+        goToSleep(mLastPowerPressTime);
     }
 
     /**
@@ -165,8 +156,9 @@
                                 Log.v(TAG, "Detected a tap to turn off dialog, ignoring");
                                 mHandler.removeCallbacks(mTurnOffDialog);
                             }
+                            showDialog(eventTime, "Enroll Power Press");
+                            mHandler.postDelayed(mTurnOffDialog, mDismissDialogTimeout);
                         });
-                showDialog(eventTime, "Enroll Power Press");
                 return true;
             case STATE_BP_AUTH:
                 return true;
@@ -176,54 +168,11 @@
         }
     }
 
-    @NonNull
-    private static Dialog showConfirmDialog(
-            @NonNull AlertDialog.Builder dialogBuilder,
-            @NonNull PowerManager powerManager,
-            long eventTime,
-            @BiometricStateListener.State int biometricState,
-            @NonNull DialogInterface.OnDismissListener dismissListener) {
-        final boolean enrolling = biometricState == STATE_ENROLLING;
-        final int title =
-                enrolling
-                        ? R.string.fp_power_button_enrollment_title
-                        : R.string.fp_power_button_bp_title;
-        final int message =
-                enrolling
-                        ? R.string.fp_power_button_enrollment_message
-                        : R.string.fp_power_button_bp_message;
-        final int positiveText =
-                enrolling
-                        ? R.string.fp_power_button_enrollment_positive_button
-                        : R.string.fp_power_button_bp_positive_button;
-        final int negativeText =
-                enrolling
-                        ? R.string.fp_power_button_enrollment_negative_button
-                        : R.string.fp_power_button_bp_negative_button;
-
-        final Dialog confirmScreenOffDialog =
-                dialogBuilder
-                        .setTitle(title)
-                        .setMessage(message)
-                        .setPositiveButton(
-                                positiveText,
-                                (dialog, which) -> {
-                                    dialog.dismiss();
-                                    powerManager.goToSleep(
-                                            eventTime,
-                                            PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON,
-                                            0 /* flags */);
-                                })
-                        .setNegativeButton(negativeText, (dialog, which) -> dialog.dismiss())
-                        .setOnDismissListener(dismissListener)
-                        .setCancelable(false)
-                        .create();
-        confirmScreenOffDialog
-                .getWindow()
-                .setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL);
-        confirmScreenOffDialog.show();
-
-        return confirmScreenOffDialog;
+    private void goToSleep(long eventTime) {
+        mPowerManager.goToSleep(
+                eventTime,
+                PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON,
+                0 /* flags */);
     }
 
     /**
@@ -247,7 +196,8 @@
                         if (fingerprintManager.isPowerbuttonFps()) {
                             fingerprintManager.registerBiometricStateListener(
                                     new BiometricStateListener() {
-                                        @Nullable private Runnable mStateRunnable = null;
+                                        @Nullable
+                                        private Runnable mStateRunnable = null;
 
                                         @Override
                                         public void onStateChanged(
@@ -281,13 +231,6 @@
                                         public void onBiometricAction(
                                                 @BiometricStateListener.Action int action) {
                                             Log.d(TAG, "onBiometricAction " + action);
-                                            switch (action) {
-                                                case BiometricStateListener.ACTION_SENSOR_TOUCH:
-                                                    mHandler.postDelayed(
-                                                            mTurnOffDialog,
-                                                            mTapWaitForPowerDuration);
-                                                    break;
-                                            }
                                         }
                                     });
                             mSideFpsEventHandlerReady.set(true);
@@ -309,12 +252,13 @@
             Log.d(TAG, "Ignoring show dialog");
             return;
         }
-        mDialog =
-                showConfirmDialog(
-                        mDialogSupplier.get(),
-                        mPowerManager,
-                        time,
-                        mBiometricState,
-                        mDialogDismissListener);
+        mDialog = mDialogProvider.provideDialog(mContext);
+        mLastPowerPressTime = time;
+        mDialog.show();
+        mDialog.setOnClickListener(this);
     }
-}
+
+    interface DialogProvider {
+        SideFpsToast provideDialog(Context context);
+    }
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/policy/SideFpsToast.java b/services/core/java/com/android/server/policy/SideFpsToast.java
new file mode 100644
index 0000000..db07467
--- /dev/null
+++ b/services/core/java/com/android/server/policy/SideFpsToast.java
@@ -0,0 +1,69 @@
+/*
+ * 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.server.policy;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.os.Bundle;
+import android.view.Gravity;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.widget.Button;
+
+import com.android.internal.R;
+
+/**
+ * Toast for side fps. This is typically shown during enrollment
+ * when a user presses the power button.
+ *
+ * This dialog is used by {@link SideFpsEventHandler}
+ */
+public class SideFpsToast extends Dialog {
+
+    SideFpsToast(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.side_fps_toast);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        final Window window = this.getWindow();
+        WindowManager.LayoutParams windowParams = window.getAttributes();
+        windowParams.dimAmount = 0;
+        windowParams.flags |= WindowManager.LayoutParams.FLAG_DIM_BEHIND;
+        windowParams.gravity = Gravity.BOTTOM;
+        window.setAttributes(windowParams);
+    }
+
+    /**
+     * Sets the onClickListener for the toast dialog.
+     * @param listener
+     */
+    public void setOnClickListener(View.OnClickListener listener) {
+        final Button turnOffScreen = findViewById(R.id.turn_off_screen);
+        if (turnOffScreen != null) {
+            turnOffScreen.setOnClickListener(listener);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 2ba0e23..75552e0 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -489,6 +489,7 @@
     static final int FLAG_FORCE_HIDDEN_FOR_PINNED_TASK = 1;
     static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1;
     private int mForceHiddenFlags = 0;
+    private boolean mForceTranslucent = false;
 
     // TODO(b/160201781): Revisit double invocation issue in Task#removeChild.
     /**
@@ -4348,6 +4349,10 @@
         return true;
     }
 
+    void setForceTranslucent(boolean set) {
+        mForceTranslucent = set;
+    }
+
     @Override
     public boolean isAlwaysOnTop() {
         return !isForceHidden() && super.isAlwaysOnTop();
@@ -4366,6 +4371,11 @@
     }
 
     @Override
+    protected boolean isForceTranslucent() {
+        return mForceTranslucent;
+    }
+
+    @Override
     long getProtoFieldId() {
         return TASK;
     }
@@ -5339,7 +5349,7 @@
                     abort = true;
                 }
                 if (abort) {
-                    Slog.e(TAG, "Cannot navigateUpTo, intent =" + destIntent);
+                    android.util.EventLog.writeEvent(0x534e4554, "238605611", callingUid, "");
                     foundParentInTask = false;
                 } else {
                     parent.deliverNewIntentLocked(callingUid, destIntent, destGrants,
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 8d94324..5f85a14 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -740,6 +740,10 @@
         return false;
     }
 
+    protected boolean isForceTranslucent() {
+        return false;
+    }
+
     boolean isLeafTaskFragment() {
         for (int i = mChildren.size() - 1; i >= 0; --i) {
             if (mChildren.get(i).asTaskFragment() != null) {
@@ -865,7 +869,7 @@
      */
     @VisibleForTesting
     boolean isTranslucent(ActivityRecord starting) {
-        if (!isAttached() || isForceHidden()) {
+        if (!isAttached() || isForceHidden() || isForceTranslucent()) {
             return true;
         }
         final PooledPredicate p = PooledLambda.obtainPredicate(TaskFragment::isOpaqueActivity,
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 29e407f..5a2100d 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -645,6 +645,12 @@
             }
         }
 
+        if ((c.getChangeMask()
+                & WindowContainerTransaction.Change.CHANGE_FORCE_TRANSLUCENT) != 0) {
+            tr.setForceTranslucent(c.getForceTranslucent());
+            effects = TRANSACT_EFFECTS_LIFECYCLE;
+        }
+
         final int childWindowingMode = c.getActivityWindowingMode();
         if (childWindowingMode > -1) {
             tr.setActivityWindowingMode(childWindowingMode);
diff --git a/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java b/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java
index 7746bd6..0635cc4 100644
--- a/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/SideFpsEventHandlerTest.java
@@ -24,7 +24,6 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.app.AlertDialog;
 import android.content.pm.PackageManager;
 import android.hardware.biometrics.BiometricStateListener;
 import android.hardware.fingerprint.FingerprintManager;
@@ -36,10 +35,13 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableContext;
+import android.testing.TestableResources;
 import android.view.Window;
 
 import androidx.test.InstrumentationRegistry;
 
+import com.android.internal.R;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -47,7 +49,6 @@
 import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
 
 import java.util.List;
 
@@ -68,17 +69,22 @@
                     BiometricStateListener.STATE_BP_AUTH,
                     BiometricStateListener.STATE_AUTH_OTHER);
 
+    private static final Integer AUTO_DISMISS_DIALOG = 500;
+
     @Rule
     public TestableContext mContext =
             new TestableContext(InstrumentationRegistry.getContext(), null);
 
-    @Mock private PackageManager mPackageManager;
-    @Mock private FingerprintManager mFingerprintManager;
-    @Spy private AlertDialog.Builder mDialogBuilder = new AlertDialog.Builder(mContext);
-    @Mock private AlertDialog mAlertDialog;
-    @Mock private Window mWindow;
+    @Mock
+    private PackageManager mPackageManager;
+    @Mock
+    private FingerprintManager mFingerprintManager;
+    @Mock
+    private SideFpsToast mDialog;
+    @Mock
+    private Window mWindow;
 
-    private TestLooper mLooper = new TestLooper();
+    private final TestLooper mLooper = new TestLooper();
     private SideFpsEventHandler mEventHandler;
     private BiometricStateListener mBiometricStateListener;
 
@@ -88,16 +94,17 @@
 
         mContext.addMockSystemService(PackageManager.class, mPackageManager);
         mContext.addMockSystemService(FingerprintManager.class, mFingerprintManager);
+        TestableResources resources = mContext.getOrCreateTestableResources();
+        resources.addOverride(R.integer.config_sideFpsToastTimeout, AUTO_DISMISS_DIALOG);
 
-        when(mDialogBuilder.create()).thenReturn(mAlertDialog);
-        when(mAlertDialog.getWindow()).thenReturn(mWindow);
+        when(mDialog.getWindow()).thenReturn(mWindow);
 
         mEventHandler =
                 new SideFpsEventHandler(
                         mContext,
                         new Handler(mLooper.getLooper()),
                         mContext.getSystemService(PowerManager.class),
-                        () -> mDialogBuilder);
+                        (ctx) -> mDialog);
     }
 
     @Test
@@ -108,7 +115,7 @@
         assertThat(mEventHandler.shouldConsumeSinglePress(60L)).isFalse();
 
         mLooper.dispatchAll();
-        verify(mAlertDialog, never()).show();
+        verify(mDialog, never()).show();
     }
 
     @Test
@@ -120,7 +127,7 @@
             assertThat(mEventHandler.shouldConsumeSinglePress(200L)).isFalse();
 
             mLooper.dispatchAll();
-            verify(mAlertDialog, never()).show();
+            verify(mDialog, never()).show();
         }
     }
 
@@ -133,7 +140,7 @@
             assertThat(mEventHandler.shouldConsumeSinglePress(400L)).isFalse();
 
             mLooper.dispatchAll();
-            verify(mAlertDialog, never()).show();
+            verify(mDialog, never()).show();
         }
     }
 
@@ -148,7 +155,7 @@
         assertThat(mEventHandler.shouldConsumeSinglePress(90000L)).isFalse();
 
         mLooper.dispatchAll();
-        verify(mAlertDialog, never()).show();
+        verify(mDialog, never()).show();
     }
 
     @Test
@@ -159,18 +166,18 @@
         assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isFalse();
 
         mLooper.dispatchAll();
-        verify(mAlertDialog, never()).show();
+        verify(mDialog, never()).show();
     }
 
     @Test
-    public void promptsWhenBPisActive() throws Exception {
+    public void doesNotpromptWhenBPisActive() throws Exception {
         setupWithSensor(true /* hasSideFps */, true /* initialized */);
 
         setBiometricState(BiometricStateListener.STATE_BP_AUTH);
         assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue();
 
         mLooper.dispatchAll();
-        verify(mAlertDialog, never()).show();
+        verify(mDialog, never()).show();
     }
 
     @Test
@@ -181,57 +188,40 @@
         assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue();
 
         mLooper.dispatchAll();
-        verify(mAlertDialog).show();
-        verify(mAlertDialog, never()).dismiss();
+        verify(mDialog).show();
     }
 
     @Test
-    public void dismissesDialogOnTouchWhenEnrolling() throws Exception {
+    public void dialogDismissesAfterTime() throws Exception {
         setupWithSensor(true /* hasSfps */, true /* initialized */);
 
         setBiometricState(BiometricStateListener.STATE_ENROLLING);
-        when(mAlertDialog.isShowing()).thenReturn(true);
+        when(mDialog.isShowing()).thenReturn(true);
         assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue();
 
         mLooper.dispatchAll();
-        verify(mAlertDialog).show();
-
-        mBiometricStateListener.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
-        mLooper.moveTimeForward(10000);
+        verify(mDialog).show();
+        mLooper.moveTimeForward(AUTO_DISMISS_DIALOG);
         mLooper.dispatchAll();
-
-        verify(mAlertDialog).dismiss();
+        verify(mDialog).dismiss();
     }
 
     @Test
-    public void dismissesDialogFailsWhenPowerPressedAndDialogShowing() throws Exception {
+    public void dialogDoesNotDismissOnSensorTouch() throws Exception {
         setupWithSensor(true /* hasSfps */, true /* initialized */);
 
         setBiometricState(BiometricStateListener.STATE_ENROLLING);
-        when(mAlertDialog.isShowing()).thenReturn(true);
+        when(mDialog.isShowing()).thenReturn(true);
         assertThat(mEventHandler.shouldConsumeSinglePress(80000L)).isTrue();
 
         mLooper.dispatchAll();
-        verify(mAlertDialog).show();
+        verify(mDialog).show();
 
         mBiometricStateListener.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
-        assertThat(mEventHandler.shouldConsumeSinglePress(60L)).isTrue();
-
+        mLooper.moveTimeForward(AUTO_DISMISS_DIALOG - 1);
         mLooper.dispatchAll();
-        verify(mAlertDialog, never()).dismiss();
-    }
 
-    @Test
-    public void showDialogAfterTap() throws Exception {
-        setupWithSensor(true /* hasSfps */, true /* initialized */);
-
-        setBiometricState(BiometricStateListener.STATE_ENROLLING);
-        when(mAlertDialog.isShowing()).thenReturn(true);
-        mBiometricStateListener.onBiometricAction(BiometricStateListener.ACTION_SENSOR_TOUCH);
-        assertThat(mEventHandler.shouldConsumeSinglePress(60L)).isTrue();
-
-        mLooper.dispatchAll();
-        verify(mAlertDialog).show();
+        verify(mDialog, never()).dismiss();
     }
 
     private void setBiometricState(@BiometricStateListener.State int newState) {
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 84c2c55..7d4e6fa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -578,6 +578,22 @@
     }
 
     @Test
+    public void testContainerTranslucentChanges() {
+        removeGlobalMinSizeRestriction();
+        final Task rootTask = new TaskBuilder(mSupervisor).setCreateActivity(true)
+                .setWindowingMode(WINDOWING_MODE_FULLSCREEN).build();
+        final ActivityRecord activity = new ActivityBuilder(mAtm).setTask(rootTask).build();
+        WindowContainerTransaction t = new WindowContainerTransaction();
+        assertFalse(rootTask.isTranslucent(activity));
+        t.setForceTranslucent(rootTask.mRemoteToken.toWindowContainerToken(), true);
+        mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
+        assertTrue(rootTask.isTranslucent(activity));
+        t.setForceTranslucent(rootTask.mRemoteToken.toWindowContainerToken(), false);
+        mWm.mAtmService.mWindowOrganizerController.applyTransaction(t);
+        assertFalse(rootTask.isTranslucent(activity));
+    }
+
+    @Test
     public void testSetIgnoreOrientationRequest_taskDisplayArea() {
         removeGlobalMinSizeRestriction();
         final TaskDisplayArea taskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea();