Merge "Initialize horizontal panel as GONE and add logs" into tm-qpr-dev
diff --git a/core/java/Android.bp b/core/java/Android.bp
index 77589a2..a7d4342b 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -15,14 +15,6 @@
         "**/*.java",
         "**/*.aidl",
     ],
-    exclude_srcs: [
-        // Remove election toolbar code from build time
-        "android/service/selectiontoolbar/*.aidl",
-        "android/service/selectiontoolbar/*.java",
-        "android/view/selectiontoolbar/*.aidl",
-        "android/view/selectiontoolbar/*.java",
-        "com/android/internal/widget/floatingtoolbar/RemoteFloatingToolbarPopup.java",
-    ],
     visibility: ["//frameworks/base"],
 }
 
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/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 4019283..6615374 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -230,6 +230,8 @@
 import android.view.contentcapture.IContentCaptureManager;
 import android.view.displayhash.DisplayHashManager;
 import android.view.inputmethod.InputMethodManager;
+import android.view.selectiontoolbar.ISelectionToolbarManager;
+import android.view.selectiontoolbar.SelectionToolbarManager;
 import android.view.textclassifier.TextClassificationManager;
 import android.view.textservice.TextServicesManager;
 import android.view.translation.ITranslationManager;
@@ -363,6 +365,15 @@
                 return new TextClassificationManager(ctx);
             }});
 
+        registerService(Context.SELECTION_TOOLBAR_SERVICE, SelectionToolbarManager.class,
+                new CachedServiceFetcher<SelectionToolbarManager>() {
+                    @Override
+                    public SelectionToolbarManager createService(ContextImpl ctx) {
+                        IBinder b = ServiceManager.getService(Context.SELECTION_TOOLBAR_SERVICE);
+                        return new SelectionToolbarManager(ctx.getOuterContext(),
+                                ISelectionToolbarManager.Stub.asInterface(b));
+                    }});
+
         registerService(Context.FONT_SERVICE, FontManager.class,
                 new CachedServiceFetcher<FontManager>() {
             @Override
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/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java
index d75ff2f..5721fa6 100644
--- a/core/java/android/view/SurfaceControlViewHost.java
+++ b/core/java/android/view/SurfaceControlViewHost.java
@@ -422,7 +422,7 @@
     public void relayout(WindowManager.LayoutParams attrs,
             WindowlessWindowManager.ResizeCompleteCallback callback) {
         mViewRoot.setLayoutParams(attrs, false);
-        mViewRoot.setReportNextDraw(true /* syncBuffer */);
+        mViewRoot.setReportNextDraw(true /* syncBuffer */, "scvh_relayout");
         mWm.setCompletionCallback(mViewRoot.mWindow.asBinder(), callback);
     }
 
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index fa37ca1..fff6c60 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -586,8 +586,21 @@
     int mContentCaptureEnabled = CONTENT_CAPTURE_ENABLED_NOT_CHECKED;
     boolean mPerformContentCapture;
 
-
     boolean mReportNextDraw;
+    /** Set only while mReportNextDraw=true, indicating the last reason that was triggered */
+    String mLastReportNextDrawReason;
+    /** The reaason the last call to performDraw() returned false */
+    String mLastPerformDrawSkippedReason;
+    /** The reason the last call to performTraversals() returned without drawing */
+    String mLastPerformTraversalsSkipDrawReason;
+    /** The state of the local sync, if one is in progress. Can be one of the states below. */
+    int mLocalSyncState;
+
+    // The possible states of the local sync, see createSyncIfNeeded()
+    private final int LOCAL_SYNC_NONE = 0;
+    private final int LOCAL_SYNC_PENDING = 1;
+    private final int LOCAL_SYNC_RETURNED = 2;
+    private final int LOCAL_SYNC_MERGED = 3;
 
     /**
      * Set whether the draw should send the buffer to system server. When set to true, VRI will
@@ -1811,7 +1824,7 @@
         mSyncSeqId = args.argi4 > mSyncSeqId ? args.argi4 : mSyncSeqId;
 
         if (msg == MSG_RESIZED_REPORT) {
-            reportNextDraw();
+            reportNextDraw("resized");
         }
 
         if (mView != null && (frameChanged || configChanged)) {
@@ -2716,6 +2729,8 @@
     }
 
     private void performTraversals() {
+        mLastPerformTraversalsSkipDrawReason = null;
+
         // cache mView since it is used so much below...
         final View host = mView;
         if (DBG) {
@@ -2725,12 +2740,14 @@
         }
 
         if (host == null || !mAdded) {
+            mLastPerformTraversalsSkipDrawReason = host == null ? "no_host" : "not_added";
             return;
         }
 
         mIsInTraversal = true;
         mWillDrawSoon = true;
         boolean cancelDraw = false;
+        String cancelReason = null;
         boolean isSyncRequest = false;
 
         boolean windowSizeMayChange = false;
@@ -3013,13 +3030,14 @@
                 relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
                 cancelDraw = (relayoutResult & RELAYOUT_RES_CANCEL_AND_REDRAW)
                         == RELAYOUT_RES_CANCEL_AND_REDRAW;
+                cancelReason = "relayout";
                 final boolean dragResizing = mPendingDragResizing;
                 if (mSyncSeqId > mLastSyncSeqId) {
                     mLastSyncSeqId = mSyncSeqId;
                     if (DEBUG_BLAST) {
                         Log.d(mTag, "Relayout called with blastSync");
                     }
-                    reportNextDraw();
+                    reportNextDraw("relayout");
                     mSyncBuffer = true;
                     isSyncRequest = true;
                     if (!cancelDraw) {
@@ -3117,6 +3135,7 @@
                             }
                         } catch (OutOfResourcesException e) {
                             handleOutOfResourcesException(e);
+                            mLastPerformTraversalsSkipDrawReason = "oom_initialize_renderer";
                             return;
                         }
                     }
@@ -3154,6 +3173,7 @@
                         mAttachInfo.mThreadedRenderer.updateSurface(mSurface);
                     } catch (OutOfResourcesException e) {
                         handleOutOfResourcesException(e);
+                        mLastPerformTraversalsSkipDrawReason = "oom_update_surface";
                         return;
                     }
                 }
@@ -3319,6 +3339,7 @@
             if (mCheckIfCanDraw) {
                 try {
                     cancelDraw = mWindowSession.cancelDraw(mWindow);
+                    cancelReason = "wm_sync";
                     if (DEBUG_BLAST) {
                         Log.d(mTag, "cancelDraw returned " + cancelDraw);
                     }
@@ -3541,19 +3562,21 @@
         mImeFocusController.onTraversal(hasWindowFocus, mWindowAttributes);
 
         if ((relayoutResult & WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
-            reportNextDraw();
+            reportNextDraw("first_relayout");
         }
 
         mCheckIfCanDraw = isSyncRequest || cancelDraw;
 
-        boolean cancelAndRedraw =
-                mAttachInfo.mTreeObserver.dispatchOnPreDraw() || (cancelDraw && mDrewOnceForSync);
+        boolean cancelDueToPreDrawListener = mAttachInfo.mTreeObserver.dispatchOnPreDraw();
+        boolean cancelAndRedraw = cancelDueToPreDrawListener
+                 || (cancelDraw && mDrewOnceForSync);
         if (!cancelAndRedraw) {
             createSyncIfNeeded();
             mDrewOnceForSync = true;
         }
 
         if (!isViewVisible) {
+            mLastPerformTraversalsSkipDrawReason = "view_not_visible";
             if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                 for (int i = 0; i < mPendingTransitions.size(); ++i) {
                     mPendingTransitions.get(i).endChangingAnimations();
@@ -3565,6 +3588,9 @@
                 mSyncBufferCallback.onBufferReady(null);
             }
         } else if (cancelAndRedraw) {
+            mLastPerformTraversalsSkipDrawReason = cancelDueToPreDrawListener
+                ? "predraw_" + mAttachInfo.mTreeObserver.getLastDispatchOnPreDrawCanceledReason()
+                : "cancel_" + cancelReason;
             // Try again
             scheduleTraversals();
         } else {
@@ -3588,11 +3614,13 @@
 
         if (!cancelAndRedraw) {
             mReportNextDraw = false;
+            mLastReportNextDrawReason = null;
             mSyncBufferCallback = null;
             mSyncBuffer = false;
             if (isInLocalSync()) {
                 mSurfaceSyncer.markSyncReady(mSyncId);
                 mSyncId = UNSET_SYNC_ID;
+                mLocalSyncState = LOCAL_SYNC_NONE;
             }
         }
     }
@@ -3604,9 +3632,12 @@
         }
 
         final int seqId = mSyncSeqId;
+        mLocalSyncState = LOCAL_SYNC_PENDING;
         mSyncId = mSurfaceSyncer.setupSync(transaction -> {
+            mLocalSyncState = LOCAL_SYNC_RETURNED;
             // Callback will be invoked on executor thread so post to main thread.
             mHandler.postAtFrontOfQueue(() -> {
+                mLocalSyncState = LOCAL_SYNC_MERGED;
                 mSurfaceChangedTransaction.merge(transaction);
                 reportDrawFinished(seqId);
             });
@@ -4321,9 +4352,12 @@
     }
 
     private boolean performDraw() {
+        mLastPerformDrawSkippedReason = null;
         if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) {
+            mLastPerformDrawSkippedReason = "screen_off";
             return false;
         } else if (mView == null) {
+            mLastPerformDrawSkippedReason = "no_root_view";
             return false;
         }
 
@@ -8390,6 +8424,21 @@
         if (mTraversalScheduled) {
             writer.println(innerPrefix + " (barrier=" + mTraversalBarrier + ")");
         }
+        writer.println(innerPrefix + "mReportNextDraw=" + mReportNextDraw);
+        if (mReportNextDraw) {
+            writer.println(innerPrefix + " (reason=" + mLastReportNextDrawReason + ")");
+        }
+        if (mLastPerformTraversalsSkipDrawReason != null) {
+            writer.println(innerPrefix + "mLastPerformTraversalsFailedReason="
+                + mLastPerformTraversalsSkipDrawReason);
+        }
+        if (mLastPerformDrawSkippedReason != null) {
+            writer.println(innerPrefix + "mLastPerformDrawFailedReason="
+                + mLastPerformDrawSkippedReason);
+        }
+        if (mLocalSyncState != LOCAL_SYNC_NONE) {
+            writer.println(innerPrefix + "mLocalSyncState=" + mLocalSyncState);
+        }
         writer.println(innerPrefix + "mIsAmbientMode="  + mIsAmbientMode);
         writer.println(innerPrefix + "mUnbufferedInputSource="
                 + Integer.toHexString(mUnbufferedInputSource));
@@ -9890,11 +9939,12 @@
         }
     }
 
-    private void reportNextDraw() {
+    private void reportNextDraw(String reason) {
         if (DEBUG_BLAST) {
             Log.d(mTag, "reportNextDraw " + Debug.getCallers(5));
         }
         mReportNextDraw = true;
+        mLastReportNextDrawReason = reason;
     }
 
     /**
@@ -9907,11 +9957,12 @@
      * @param syncBuffer If true, the transaction that contains the buffer from the draw should be
      *                   sent to system to be synced. If false, VRI will not try to sync the buffer,
      *                   but only report back that a buffer was drawn.
+     * @param reason A debug string indicating the reason for reporting the next draw
      * @hide
      */
-    public void setReportNextDraw(boolean syncBuffer) {
+    public void setReportNextDraw(boolean syncBuffer, String reason) {
         mSyncBuffer = syncBuffer;
-        reportNextDraw();
+        reportNextDraw(reason);
         invalidate();
     }
 
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/ViewTreeObserver.java b/core/java/android/view/ViewTreeObserver.java
index ed8350a..fd62ecd 100644
--- a/core/java/android/view/ViewTreeObserver.java
+++ b/core/java/android/view/ViewTreeObserver.java
@@ -74,6 +74,9 @@
      * that the listener will be immediately called. */
     private boolean mWindowShown;
 
+    // The reason that the last call to dispatchOnPreDraw() returned true to cancel and redraw
+    private String mLastDispatchOnPreDrawCanceledReason;
+
     private boolean mAlive = true;
 
     /**
@@ -1167,6 +1170,7 @@
      */
     @SuppressWarnings("unchecked")
     public final boolean dispatchOnPreDraw() {
+        mLastDispatchOnPreDrawCanceledReason = null;
         boolean cancelDraw = false;
         final CopyOnWriteArray<OnPreDrawListener> listeners = mOnPreDrawListeners;
         if (listeners != null && listeners.size() > 0) {
@@ -1174,7 +1178,11 @@
             try {
                 int count = access.size();
                 for (int i = 0; i < count; i++) {
-                    cancelDraw |= !(access.get(i).onPreDraw());
+                    final OnPreDrawListener preDrawListener = access.get(i);
+                    cancelDraw |= !(preDrawListener.onPreDraw());
+                    if (cancelDraw) {
+                        mLastDispatchOnPreDrawCanceledReason = preDrawListener.getClass().getName();
+                    }
                 }
             } finally {
                 listeners.end();
@@ -1184,6 +1192,15 @@
     }
 
     /**
+     * @return the reason that the last call to dispatchOnPreDraw() returned true to cancel the
+     *         current draw, or null if the last call did not cancel.
+     * @hide
+     */
+    final String getLastDispatchOnPreDrawCanceledReason() {
+        return mLastDispatchOnPreDrawCanceledReason;
+    }
+
+    /**
      * Notifies registered listeners that the window is now shown
      * @hide
      */
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/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java
index 5eec054..a339062 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -1587,7 +1587,13 @@
 
         public BitmapCache(Parcel source) {
             mBitmaps = source.createTypedArrayList(Bitmap.CREATOR);
-            mBitmapHashes = source.readSparseIntArray();
+            mBitmapHashes = new SparseIntArray();
+            for (int i = 0; i < mBitmaps.size(); i++) {
+                Bitmap b = mBitmaps.get(i);
+                if (b != null) {
+                    mBitmapHashes.put(b.hashCode(), i);
+                }
+            }
         }
 
         public int getBitmapId(Bitmap b) {
@@ -1603,7 +1609,7 @@
                         b = b.asShared();
                     }
                     mBitmaps.add(b);
-                    mBitmapHashes.put(mBitmaps.size() - 1, hash);
+                    mBitmapHashes.put(hash, mBitmaps.size() - 1);
                     mBitmapMemory = -1;
                     return (mBitmaps.size() - 1);
                 }
@@ -1620,7 +1626,6 @@
 
         public void writeBitmapsToParcel(Parcel dest, int flags) {
             dest.writeTypedList(mBitmaps, flags);
-            dest.writeSparseIntArray(mBitmapHashes);
         }
 
         public int getBitmapMemory() {
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/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index 1d396be..0730f3d 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -325,7 +325,8 @@
         public boolean checkApplicationCallbackRegistration(int priority,
                 OnBackInvokedCallback callback) {
             if (!mApplicationCallBackEnabled
-                    && !(callback instanceof CompatOnBackInvokedCallback)) {
+                    && !(callback instanceof CompatOnBackInvokedCallback)
+                    && !ALWAYS_ENFORCE_PREDICTIVE_BACK) {
                 Log.w("OnBackInvokedCallback",
                         "OnBackInvokedCallback is not enabled for the application."
                                 + "\nSet 'android:enableOnBackInvokedCallback=\"true\"' in the"
diff --git a/core/java/com/android/internal/widget/floatingtoolbar/FloatingToolbarPopup.java b/core/java/com/android/internal/widget/floatingtoolbar/FloatingToolbarPopup.java
index c484525..f7af67b 100644
--- a/core/java/com/android/internal/widget/floatingtoolbar/FloatingToolbarPopup.java
+++ b/core/java/com/android/internal/widget/floatingtoolbar/FloatingToolbarPopup.java
@@ -21,6 +21,7 @@
 import android.graphics.Rect;
 import android.view.MenuItem;
 import android.view.View;
+import android.view.selectiontoolbar.SelectionToolbarManager;
 import android.widget.PopupWindow;
 
 import java.util.List;
@@ -92,7 +93,10 @@
      * enabled, otherwise returns {@link LocalFloatingToolbarPopup} implementation.
      */
     static FloatingToolbarPopup createInstance(Context context, View parent) {
-        return new LocalFloatingToolbarPopup(context, parent);
+        boolean enabled = SelectionToolbarManager.isRemoteSelectionToolbarEnabled(context);
+        return enabled
+                ? new RemoteFloatingToolbarPopup(context, parent)
+                : new LocalFloatingToolbarPopup(context, parent);
     }
 
 }
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/ShellTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
index 5cba3b4..6ae0f9b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -55,6 +55,7 @@
 import com.android.wm.shell.compatui.CompatUIController;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.startingsurface.StartingWindowController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.unfold.UnfoldAnimationController;
 
@@ -72,6 +73,7 @@
  */
 public class ShellTaskOrganizer extends TaskOrganizer implements
         CompatUIController.CompatUICallback {
+    private static final String TAG = "ShellTaskOrganizer";
 
     // Intentionally using negative numbers here so the positive numbers can be used
     // for task id specific listeners that will be added later.
@@ -90,8 +92,6 @@
     })
     public @interface TaskListenerType {}
 
-    private static final String TAG = "ShellTaskOrganizer";
-
     /**
      * Callbacks for when the tasks change in the system.
      */
@@ -177,6 +177,9 @@
     @Nullable
     private final CompatUIController mCompatUI;
 
+    @NonNull
+    private final ShellCommandHandler mShellCommandHandler;
+
     @Nullable
     private final Optional<RecentTasksController> mRecentTasks;
 
@@ -187,29 +190,33 @@
     private RunningTaskInfo mLastFocusedTaskInfo;
 
     public ShellTaskOrganizer(ShellExecutor mainExecutor) {
-        this(null /* shellInit */, null /* taskOrganizerController */, null /* compatUI */,
+        this(null /* shellInit */, null /* shellCommandHandler */,
+                null /* taskOrganizerController */, null /* compatUI */,
                 Optional.empty() /* unfoldAnimationController */,
                 Optional.empty() /* recentTasksController */,
                 mainExecutor);
     }
 
     public ShellTaskOrganizer(ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             @Nullable CompatUIController compatUI,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasks,
             ShellExecutor mainExecutor) {
-        this(shellInit, null /* taskOrganizerController */, compatUI,
+        this(shellInit, shellCommandHandler, null /* taskOrganizerController */, compatUI,
                 unfoldAnimationController, recentTasks, mainExecutor);
     }
 
     @VisibleForTesting
     protected ShellTaskOrganizer(ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ITaskOrganizerController taskOrganizerController,
             @Nullable CompatUIController compatUI,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasks,
             ShellExecutor mainExecutor) {
         super(taskOrganizerController, mainExecutor);
+        mShellCommandHandler = shellCommandHandler;
         mCompatUI = compatUI;
         mRecentTasks = recentTasks;
         mUnfoldAnimationController = unfoldAnimationController.orElse(null);
@@ -219,6 +226,7 @@
     }
 
     private void onInit() {
+        mShellCommandHandler.addDumpCallback(this::dump, this);
         if (mCompatUI != null) {
             mCompatUI.setCompatUICallback(this);
         }
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/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
index 05fafc5..d3e46f8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java
@@ -57,6 +57,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.annotations.ShellBackgroundThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -90,13 +91,14 @@
      * Raw delta between {@link #mInitTouchLocation} and the last touch location.
      */
     private final Point mTouchEventDelta = new Point();
-    private final ShellExecutor mShellExecutor;
 
     /** True when a back gesture is ongoing */
     private boolean mBackGestureStarted = false;
 
     /** Tracks if an uninterruptible transition is in progress */
     private boolean mTransitionInProgress = false;
+    /** Tracks if we should start the back gesture on the next motion move event */
+    private boolean mShouldStartOnNextMoveEvent = false;
     /** @see #setTriggerBack(boolean) */
     private boolean mTriggerBack;
 
@@ -105,6 +107,9 @@
     private final SurfaceControl.Transaction mTransaction;
     private final IActivityTaskManager mActivityTaskManager;
     private final Context mContext;
+    private final ContentResolver mContentResolver;
+    private final ShellExecutor mShellExecutor;
+    private final Handler mBgHandler;
     @Nullable
     private IOnBackInvokedCallback mBackToLauncherCallback;
     private float mTriggerThreshold;
@@ -133,16 +138,19 @@
     };
 
     public BackAnimationController(
+            @NonNull ShellInit shellInit,
             @NonNull @ShellMainThread ShellExecutor shellExecutor,
             @NonNull @ShellBackgroundThread Handler backgroundHandler,
             Context context) {
-        this(shellExecutor, backgroundHandler, new SurfaceControl.Transaction(),
+        this(shellInit, shellExecutor, backgroundHandler, new SurfaceControl.Transaction(),
                 ActivityTaskManager.getService(), context, context.getContentResolver());
     }
 
     @VisibleForTesting
-    BackAnimationController(@NonNull @ShellMainThread ShellExecutor shellExecutor,
-            @NonNull @ShellBackgroundThread Handler handler,
+    BackAnimationController(
+            @NonNull ShellInit shellInit,
+            @NonNull @ShellMainThread ShellExecutor shellExecutor,
+            @NonNull @ShellBackgroundThread Handler bgHandler,
             @NonNull SurfaceControl.Transaction transaction,
             @NonNull IActivityTaskManager activityTaskManager,
             Context context, ContentResolver contentResolver) {
@@ -150,7 +158,13 @@
         mTransaction = transaction;
         mActivityTaskManager = activityTaskManager;
         mContext = context;
-        setupAnimationDeveloperSettingsObserver(contentResolver, handler);
+        mContentResolver = contentResolver;
+        mBgHandler = bgHandler;
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        setupAnimationDeveloperSettingsObserver(mContentResolver, mBgHandler);
     }
 
     private void setupAnimationDeveloperSettingsObserver(
@@ -286,12 +300,17 @@
         if (mTransitionInProgress) {
             return;
         }
-        if (keyAction == MotionEvent.ACTION_MOVE) {
+        if (keyAction == MotionEvent.ACTION_DOWN) {
             if (!mBackGestureStarted) {
+                mShouldStartOnNextMoveEvent = true;
+            }
+        } else if (keyAction == MotionEvent.ACTION_MOVE) {
+            if (!mBackGestureStarted && mShouldStartOnNextMoveEvent) {
                 // Let the animation initialized here to make sure the onPointerDownOutsideFocus
                 // could be happened when ACTION_DOWN, it may change the current focus that we
                 // would access it when startBackNavigation.
                 onGestureStarted(touchX, touchY);
+                mShouldStartOnNextMoveEvent = false;
             }
             onMove(touchX, touchY, swipeEdge);
         } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
@@ -425,6 +444,11 @@
 
     private void onGestureFinished(boolean fromTouch) {
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
+        if (!mBackGestureStarted) {
+            finishAnimation();
+            return;
+        }
+
         if (fromTouch) {
             // Let touch reset the flag otherwise it will start a new back navigation and refresh
             // the info when received a new move event.
@@ -540,6 +564,7 @@
         boolean triggerBack = mTriggerBack;
         mBackNavigationInfo = null;
         mTriggerBack = false;
+        mShouldStartOnNextMoveEvent = false;
         if (backNavigationInfo == null) {
             return;
         }
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/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
index db8d9d4..235fd9c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java
@@ -43,6 +43,7 @@
 import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
 import java.lang.ref.WeakReference;
@@ -119,6 +120,7 @@
     private boolean mKeyguardShowing;
 
     public CompatUIController(Context context,
+            ShellInit shellInit,
             ShellController shellController,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
@@ -134,10 +136,14 @@
         mSyncQueue = syncQueue;
         mMainExecutor = mainExecutor;
         mTransitionsLazy = transitionsLazy;
+        mCompatUIHintsState = new CompatUIHintsState();
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        mShellController.addKeyguardChangeListener(this);
         mDisplayController.addDisplayWindowListener(this);
         mImeController.addPositionProcessor(this);
-        mCompatUIHintsState = new CompatUIHintsState();
-        shellController.addKeyguardChangeListener(this);
     }
 
     /** Sets the callback for UI interactions. */
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 3add417..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);
     }
@@ -174,13 +165,14 @@
     @Provides
     static ShellTaskOrganizer provideShellTaskOrganizer(
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             CompatUIController compatUI,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasksOptional,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
-        return new ShellTaskOrganizer(shellInit, compatUI, unfoldAnimationController,
-                recentTasksOptional, mainExecutor);
+        return new ShellTaskOrganizer(shellInit, shellCommandHandler, compatUI,
+                unfoldAnimationController, recentTasksOptional, mainExecutor);
     }
 
     @WMSingleton
@@ -188,6 +180,7 @@
     static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
             Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             SyncTransactionQueue syncTransactionQueue,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
@@ -196,19 +189,20 @@
             @ShellMainThread ShellExecutor mainExecutor,
             @ShellMainThread Handler mainHandler
     ) {
-        return new KidsModeTaskOrganizer(context, shellInit, syncTransactionQueue,
-                displayController, displayInsetsController, unfoldAnimationController,
-                recentTasksOptional, mainExecutor, mainHandler);
+        return new KidsModeTaskOrganizer(context, shellInit, shellCommandHandler,
+                syncTransactionQueue, displayController, displayInsetsController,
+                unfoldAnimationController, recentTasksOptional, mainExecutor, mainHandler);
     }
 
     @WMSingleton
     @Provides
     static CompatUIController provideCompatUIController(Context context,
+            ShellInit shellInit,
             ShellController shellController,
             DisplayController displayController, DisplayInsetsController displayInsetsController,
             DisplayImeController imeController, SyncTransactionQueue syncQueue,
             @ShellMainThread ShellExecutor mainExecutor, Lazy<Transitions> transitionsLazy) {
-        return new CompatUIController(context, shellController, displayController,
+        return new CompatUIController(context, shellInit, shellController, displayController,
                 displayInsetsController, imeController, syncQueue, mainExecutor, transitionsLazy);
     }
 
@@ -268,12 +262,14 @@
     @Provides
     static Optional<BackAnimationController> provideBackAnimationController(
             Context context,
+            ShellInit shellInit,
             @ShellMainThread ShellExecutor shellExecutor,
             @ShellBackgroundThread Handler backgroundHandler
     ) {
         if (BackAnimationController.IS_ENABLED) {
             return Optional.of(
-                    new BackAnimationController(shellExecutor, backgroundHandler, context));
+                    new BackAnimationController(shellInit, shellExecutor, backgroundHandler,
+                            context));
         }
         return Optional.empty();
     }
@@ -384,11 +380,14 @@
     @WMSingleton
     @Provides
     static Optional<HideDisplayCutoutController> provideHideDisplayCutoutController(Context context,
-            ShellController shellController, DisplayController displayController,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
+            ShellController shellController,
+            DisplayController displayController,
             @ShellMainThread ShellExecutor mainExecutor) {
         return Optional.ofNullable(
-                HideDisplayCutoutController.create(context, shellController, displayController,
-                        mainExecutor));
+                HideDisplayCutoutController.create(context, shellInit, shellCommandHandler,
+                        shellController, displayController, mainExecutor));
     }
 
     //
@@ -466,12 +465,13 @@
     static Optional<RecentTasksController> provideRecentTasksController(
             Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             TaskStackListenerImpl taskStackListener,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
         return Optional.ofNullable(
-                RecentTasksController.create(context, shellInit, taskStackListener,
-                        mainExecutor));
+                RecentTasksController.create(context, shellInit, shellCommandHandler,
+                        taskStackListener, mainExecutor));
     }
 
     //
@@ -649,8 +649,9 @@
     @WMSingleton
     @Provides
     static ShellController provideShellController(ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             @ShellMainThread ShellExecutor mainExecutor) {
-        return new ShellController(shellInit, mainExecutor);
+        return new ShellController(shellInit, shellCommandHandler, mainExecutor);
     }
 
     //
@@ -676,12 +677,15 @@
             KidsModeTaskOrganizer kidsModeTaskOrganizer,
             Optional<BubbleController> bubblesOptional,
             Optional<SplitScreenController> splitScreenOptional,
+            Optional<Pip> pipOptional,
             Optional<PipTouchHandler> pipTouchHandlerOptional,
             FullscreenTaskListener fullscreenTaskListener,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
             Optional<FreeformComponents> freeformComponents,
             Optional<RecentTasksController> recentTasksOptional,
+            Optional<OneHandedController> oneHandedControllerOptional,
+            Optional<HideDisplayCutoutController> hideDisplayCutoutControllerOptional,
             ActivityEmbeddingController activityEmbeddingOptional,
             Transitions transitions,
             StartingWindowController startingWindow,
@@ -697,18 +701,7 @@
 
     @WMSingleton
     @Provides
-    static ShellCommandHandler provideShellCommandHandlerImpl(
-            ShellController shellController,
-            ShellTaskOrganizer shellTaskOrganizer,
-            KidsModeTaskOrganizer kidsModeTaskOrganizer,
-            Optional<SplitScreenController> splitScreenOptional,
-            Optional<Pip> pipOptional,
-            Optional<OneHandedController> oneHandedOptional,
-            Optional<HideDisplayCutoutController> hideDisplayCutout,
-            Optional<RecentTasksController> recentTasksOptional,
-            @ShellMainThread ShellExecutor mainExecutor) {
-        return new ShellCommandHandler(shellController, shellTaskOrganizer,
-                kidsModeTaskOrganizer, splitScreenOptional, pipOptional, oneHandedOptional,
-                hideDisplayCutout, recentTasksOptional, mainExecutor);
+    static ShellCommandHandler provideShellCommandHandler() {
+        return new ShellCommandHandler();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index d53451a..2ca9c3b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -74,6 +74,7 @@
 import com.android.wm.shell.pip.phone.PipTouchHandler;
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.SplitscreenPipMixedHandler;
@@ -248,14 +249,20 @@
     @Provides
     @DynamicOverride
     static OneHandedController provideOneHandedController(Context context,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
-            WindowManager windowManager, DisplayController displayController,
-            DisplayLayout displayLayout, TaskStackListenerImpl taskStackListener,
-            UiEventLogger uiEventLogger, InteractionJankMonitor jankMonitor,
-            @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) {
-        return OneHandedController.create(context, shellController, windowManager,
-                displayController, displayLayout, taskStackListener, jankMonitor, uiEventLogger,
-                mainExecutor, mainHandler);
+            WindowManager windowManager,
+            DisplayController displayController,
+            DisplayLayout displayLayout,
+            TaskStackListenerImpl taskStackListener,
+            UiEventLogger uiEventLogger,
+            InteractionJankMonitor jankMonitor,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler) {
+        return OneHandedController.create(context, shellInit, shellCommandHandler, shellController,
+                windowManager, displayController, displayLayout, taskStackListener, jankMonitor,
+                uiEventLogger, mainExecutor, mainHandler);
     }
 
     //
@@ -268,6 +275,7 @@
     static SplitScreenController provideSplitScreenController(
             Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
             ShellTaskOrganizer shellTaskOrganizer,
             SyncTransactionQueue syncQueue,
@@ -281,10 +289,10 @@
             IconProvider iconProvider,
             Optional<RecentTasksController> recentTasks,
             @ShellMainThread ShellExecutor mainExecutor) {
-        return new SplitScreenController(context, shellInit, shellController, shellTaskOrganizer,
-                syncQueue, rootTaskDisplayAreaOrganizer, displayController, displayImeController,
-                displayInsetsController, dragAndDropController, transitions, transactionPool,
-                iconProvider, recentTasks, mainExecutor);
+        return new SplitScreenController(context, shellInit, shellCommandHandler, shellController,
+                shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController,
+                displayImeController, displayInsetsController, dragAndDropController, transitions,
+                transactionPool, iconProvider, recentTasks, mainExecutor);
     }
 
     //
@@ -294,24 +302,33 @@
     @WMSingleton
     @Provides
     static Optional<Pip> providePip(Context context,
-            ShellController shellController, DisplayController displayController,
-            PipAppOpsListener pipAppOpsListener, PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipKeepClearAlgorithm pipKeepClearAlgorithm, PipBoundsState pipBoundsState,
-            PipMotionHelper pipMotionHelper, PipMediaController pipMediaController,
-            PhonePipMenuController phonePipMenuController, PipTaskOrganizer pipTaskOrganizer,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
+            ShellController shellController,
+            DisplayController displayController,
+            PipAppOpsListener pipAppOpsListener,
+            PipBoundsAlgorithm pipBoundsAlgorithm,
+            PipKeepClearAlgorithm pipKeepClearAlgorithm,
+            PipBoundsState pipBoundsState,
+            PipMotionHelper pipMotionHelper,
+            PipMediaController pipMediaController,
+            PhonePipMenuController phonePipMenuController,
+            PipTaskOrganizer pipTaskOrganizer,
             PipTransitionState pipTransitionState,
-            PipTouchHandler pipTouchHandler, PipTransitionController pipTransitionController,
+            PipTouchHandler pipTouchHandler,
+            PipTransitionController pipTransitionController,
             WindowManagerShellWrapper windowManagerShellWrapper,
             TaskStackListenerImpl taskStackListener,
             PipParamsChangedForwarder pipParamsChangedForwarder,
             Optional<OneHandedController> oneHandedController,
             @ShellMainThread ShellExecutor mainExecutor) {
-        return Optional.ofNullable(PipController.create(context, shellController, displayController,
-                pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState,
-                pipMotionHelper,
-                pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState,
-                pipTouchHandler, pipTransitionController, windowManagerShellWrapper,
-                taskStackListener, pipParamsChangedForwarder, oneHandedController, mainExecutor));
+        return Optional.ofNullable(PipController.create(
+                context, shellInit, shellCommandHandler, shellController,
+                displayController, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm,
+                pipBoundsState, pipMotionHelper, pipMediaController, phonePipMenuController,
+                pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController,
+                windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
+                oneHandedController, mainExecutor));
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
index 52f0c42..99922fb 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md
@@ -59,9 +59,9 @@
 adb shell dumpsys activity service SystemUIService WMShell
 ```
 
-If information should be added to the dump, make updates to:
-- `WMShell` if you are dumping SysUI state
-- `ShellCommandHandler` if you are dumping Shell state
+If information should be added to the dump, either:
+- Update `WMShell` if you are dumping SysUI state
+- Inject `ShellCommandHandler` into your Shell class, and add a dump callback
 
 ## Debugging in Android Studio
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
index 0dd50b1..d6302e6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md
@@ -32,7 +32,7 @@
 
 More detail can be found in [go/wm-sysui-dagger](http://go/wm-sysui-dagger).
 
-## Interfaces to Shell components
+## Interfaces from SysUI to Shell components
 
 Within the same process, the WM Shell components can be running on a different thread than the main
 SysUI thread (disabled on certain products).  This introduces challenges where we have to be
@@ -54,12 +54,30 @@
 Adding an interface to a Shell component may seem like a lot of boiler plate, but is currently
 necessary to maintain proper threading and logic isolation.
 
-## Configuration changes & other SysUI events
+## Listening for Configuration changes & other SysUI events
 
-Aside from direct calls into Shell controllers for exposed features, the Shell also receives 
+Aside from direct calls into Shell controllers for exposed features, the Shell also receives
 common event callbacks from SysUI via the `ShellController`.  This includes things like:
 
 - Configuration changes
-- TODO: Shell init
-- TODO: Shell command
-- TODO: Keyguard events
\ No newline at end of file
+- Keyguard events
+- Shell init
+- Shell dumps & commands
+
+For other events which are specific to the Shell feature, then you can add callback methods on
+the Shell feature interface.  Any such calls should <u>**never**</u> be synchronous calls as
+they will need to post to the Shell main thread to run.
+
+## Shell commands & Dumps
+
+Since the Shell library is a part of the SysUI process, it relies on SysUI to trigger commands
+on individual Shell components, or to dump individual shell components.
+
+```shell
+# Dump everything
+adb shell dumpsys activity service SystemUIService WMShell
+
+# Run a specific command
+adb shell dumpsys activity service SystemUIService WMShell help
+adb shell dumpsys activity service SystemUIService WMShell <cmd> <args> ...
+```
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java
index 665b035..32125fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutController.java
@@ -27,7 +27,9 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.io.PrintWriter;
 
@@ -38,6 +40,7 @@
     private static final String TAG = "HideDisplayCutoutController";
 
     private final Context mContext;
+    private final ShellCommandHandler mShellCommandHandler;
     private final ShellController mShellController;
     private final HideDisplayCutoutOrganizer mOrganizer;
     @VisibleForTesting
@@ -49,7 +52,10 @@
      */
     @Nullable
     public static HideDisplayCutoutController create(Context context,
-            ShellController shellController, DisplayController displayController,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
+            ShellController shellController,
+            DisplayController displayController,
             ShellExecutor mainExecutor) {
         // The SystemProperty is set for devices that support this feature and is used to control
         // whether to create the HideDisplayCutout instance.
@@ -60,14 +66,24 @@
 
         HideDisplayCutoutOrganizer organizer =
                 new HideDisplayCutoutOrganizer(context, displayController, mainExecutor);
-        return new HideDisplayCutoutController(context, shellController, organizer);
+        return new HideDisplayCutoutController(context, shellInit, shellCommandHandler,
+                shellController, organizer);
     }
 
-    HideDisplayCutoutController(Context context, ShellController shellController,
+    HideDisplayCutoutController(Context context,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
+            ShellController shellController,
             HideDisplayCutoutOrganizer organizer) {
         mContext = context;
+        mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
         mOrganizer = organizer;
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        mShellCommandHandler.addDumpCallback(this::dump, this);
         updateStatus();
         mShellController.addConfigurationChangeListener(this);
     }
@@ -96,11 +112,11 @@
         updateStatus();
     }
 
-    public void dump(@NonNull PrintWriter pw) {
-        final String prefix = "  ";
+    public void dump(@NonNull PrintWriter pw, String prefix) {
+        final String innerPrefix = "  ";
         pw.print(TAG);
         pw.println(" states: ");
-        pw.print(prefix);
+        pw.print(innerPrefix);
         pw.print("mEnabled=");
         pw.println(mEnabled);
         mOrganizer.dump(pw);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
index 73b9b89..2fdd121 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
@@ -50,6 +50,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.unfold.UnfoldAnimationController;
 
@@ -73,6 +74,7 @@
 
     private final Handler mMainHandler;
     private final Context mContext;
+    private final ShellCommandHandler mShellCommandHandler;
     private final SyncTransactionQueue mSyncQueue;
     private final DisplayController mDisplayController;
     private final DisplayInsetsController mDisplayInsetsController;
@@ -141,6 +143,8 @@
     @VisibleForTesting
     KidsModeTaskOrganizer(
             Context context,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ITaskOrganizerController taskOrganizerController,
             SyncTransactionQueue syncTransactionQueue,
             DisplayController displayController,
@@ -150,19 +154,23 @@
             KidsModeSettingsObserver kidsModeSettingsObserver,
             ShellExecutor mainExecutor,
             Handler mainHandler) {
-        super(/* shellInit= */ null, taskOrganizerController, /* compatUI= */ null,
-                unfoldAnimationController, recentTasks, mainExecutor);
+        // Note: we don't call super with the shell init because we will be initializing manually
+        super(/* shellInit= */ null, /* shellCommandHandler= */ null, taskOrganizerController,
+                /* compatUI= */ null, unfoldAnimationController, recentTasks, mainExecutor);
         mContext = context;
+        mShellCommandHandler = shellCommandHandler;
         mMainHandler = mainHandler;
         mSyncQueue = syncTransactionQueue;
         mDisplayController = displayController;
         mDisplayInsetsController = displayInsetsController;
         mKidsModeSettingsObserver = kidsModeSettingsObserver;
+        shellInit.addInitCallback(this::onInit, this);
     }
 
     public KidsModeTaskOrganizer(
             Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             SyncTransactionQueue syncTransactionQueue,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
@@ -171,9 +179,10 @@
             ShellExecutor mainExecutor,
             Handler mainHandler) {
         // Note: we don't call super with the shell init because we will be initializing manually
-        super(/* shellInit= */ null, /* compatUI= */ null, unfoldAnimationController, recentTasks,
-                mainExecutor);
+        super(/* shellInit= */ null, /* taskOrganizerController= */ null, /* compatUI= */ null,
+                unfoldAnimationController, recentTasks, mainExecutor);
         mContext = context;
+        mShellCommandHandler = shellCommandHandler;
         mMainHandler = mainHandler;
         mSyncQueue = syncTransactionQueue;
         mDisplayController = displayController;
@@ -185,6 +194,9 @@
      * Initializes kids mode status.
      */
     public void onInit() {
+        if (mShellCommandHandler != null) {
+            mShellCommandHandler.addDumpCallback(this::dump, this);
+        }
         if (mKidsModeSettingsObserver == null) {
             mKidsModeSettingsObserver = new KidsModeSettingsObserver(mMainHandler, mContext);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
index 24f02ac..9149204 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/onehanded/OneHandedController.java
@@ -56,7 +56,9 @@
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.io.PrintWriter;
 
@@ -85,6 +87,7 @@
 
     private Context mContext;
 
+    private final ShellCommandHandler mShellCommandHandler;
     private final ShellController mShellController;
     private final AccessibilityManager mAccessibilityManager;
     private final DisplayController mDisplayController;
@@ -192,8 +195,9 @@
     /**
      * Creates {@link OneHandedController}, returns {@code null} if the feature is not supported.
      */
-    public static OneHandedController create(
-            Context context, ShellController shellController, WindowManager windowManager,
+    public static OneHandedController create(Context context,
+            ShellInit shellInit, ShellCommandHandler shellCommandHandler,
+            ShellController shellController, WindowManager windowManager,
             DisplayController displayController, DisplayLayout displayLayout,
             TaskStackListenerImpl taskStackListener,
             InteractionJankMonitor jankMonitor, UiEventLogger uiEventLogger,
@@ -213,14 +217,16 @@
                 context, displayLayout, settingsUtil, animationController, tutorialHandler,
                 jankMonitor, mainExecutor);
         OneHandedUiEventLogger oneHandedUiEventsLogger = new OneHandedUiEventLogger(uiEventLogger);
-        return new OneHandedController(context, shellController, displayController, organizer,
-                touchHandler, tutorialHandler, settingsUtil, accessibilityUtil, timeoutHandler,
-                oneHandedState, oneHandedUiEventsLogger, taskStackListener,
-                mainExecutor, mainHandler);
+        return new OneHandedController(context, shellInit, shellCommandHandler, shellController,
+                displayController, organizer, touchHandler, tutorialHandler, settingsUtil,
+                accessibilityUtil, timeoutHandler, oneHandedState, oneHandedUiEventsLogger,
+                taskStackListener, mainExecutor, mainHandler);
     }
 
     @VisibleForTesting
     OneHandedController(Context context,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
             DisplayController displayController,
             OneHandedDisplayAreaOrganizer displayAreaOrganizer,
@@ -235,6 +241,7 @@
             ShellExecutor mainExecutor,
             Handler mainHandler) {
         mContext = context;
+        mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
         mOneHandedSettingsUtil = settingsUtil;
         mOneHandedAccessibilityUtil = oneHandedAccessibilityUtil;
@@ -247,8 +254,8 @@
         mMainHandler = mainHandler;
         mOneHandedUiEventLogger = uiEventsLogger;
         mTaskStackListener = taskStackListener;
+        mAccessibilityManager = AccessibilityManager.getInstance(mContext);
 
-        mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
         final float offsetPercentageConfig = context.getResources().getFraction(
                 R.fraction.config_one_handed_offset, 1, 1);
         final int sysPropPercentageConfig = SystemProperties.getInt(
@@ -268,6 +275,12 @@
                 getObserver(this::onSwipeToNotificationEnabledChanged);
         mShortcutEnabledObserver = getObserver(this::onShortcutEnabledChanged);
 
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
+        mShellCommandHandler.addDumpCallback(this::dump, this);
+        mDisplayController.addDisplayWindowListener(mDisplaysChangedListener);
         mDisplayController.addDisplayChangingController(this);
         setupCallback();
         registerSettingObservers(mUserId);
@@ -275,7 +288,6 @@
         updateSettings();
         updateDisplayLayout(mContext.getDisplayId());
 
-        mAccessibilityManager = AccessibilityManager.getInstance(context);
         mAccessibilityManager.addAccessibilityStateChangeListener(
                 mAccessibilityStateChangeListener);
 
@@ -623,7 +635,7 @@
         updateOneHandedEnabled();
     }
 
-    public void dump(@NonNull PrintWriter pw) {
+    public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = "  ";
         pw.println();
         pw.println(TAG);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
index 38631ce..93172f8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/Pip.java
@@ -20,7 +20,6 @@
 
 import com.android.wm.shell.common.annotations.ExternalThread;
 
-import java.io.PrintWriter;
 import java.util.function.Consumer;
 
 /**
@@ -99,12 +98,4 @@
      * view hierarchy or destroyed.
      */
     default void removePipExclusionBoundsChangeListener(Consumer<Rect> listener) { }
-
-    /**
-     * Dump the current state and information if need.
-     *
-     * @param pw The stream to dump information to.
-     */
-    default void dump(PrintWriter pw) {
-    }
 }
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/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 2144305..fc97f31 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -89,7 +89,9 @@
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
@@ -122,6 +124,7 @@
     private TaskStackListenerImpl mTaskStackListener;
     private PipParamsChangedForwarder mPipParamsChangedForwarder;
     private Optional<OneHandedController> mOneHandedController;
+    private final ShellCommandHandler mShellCommandHandler;
     private final ShellController mShellController;
     protected final PipImpl mImpl;
 
@@ -295,6 +298,8 @@
      */
     @Nullable
     public static Pip create(Context context,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
             DisplayController displayController,
             PipAppOpsListener pipAppOpsListener,
@@ -319,16 +324,18 @@
             return null;
         }
 
-        return new PipController(context, shellController, displayController, pipAppOpsListener,
-                pipBoundsAlgorithm, pipKeepClearAlgorithm, pipBoundsState, pipMotionHelper,
-                pipMediaController, phonePipMenuController, pipTaskOrganizer, pipTransitionState,
-                pipTouchHandler, pipTransitionController,
+        return new PipController(context, shellInit, shellCommandHandler, shellController,
+                displayController, pipAppOpsListener, pipBoundsAlgorithm, pipKeepClearAlgorithm,
+                pipBoundsState, pipMotionHelper, pipMediaController, phonePipMenuController,
+                pipTaskOrganizer, pipTransitionState, pipTouchHandler, pipTransitionController,
                 windowManagerShellWrapper, taskStackListener, pipParamsChangedForwarder,
                 oneHandedController, mainExecutor)
                 .mImpl;
     }
 
     protected PipController(Context context,
+            ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
             DisplayController displayController,
             PipAppOpsListener pipAppOpsListener,
@@ -355,6 +362,7 @@
         }
 
         mContext = context;
+        mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
         mImpl = new PipImpl();
         mWindowManagerShellWrapper = windowManagerShellWrapper;
@@ -378,11 +386,11 @@
                 .getInteger(R.integer.config_pipEnterAnimationDuration);
         mPipParamsChangedForwarder = pipParamsChangedForwarder;
 
-        //TODO: move this to ShellInit when PipController can be injected
-        mMainExecutor.execute(this::init);
+        shellInit.addInitCallback(this::onInit, this);
     }
 
-    public void init() {
+    private void onInit() {
+        mShellCommandHandler.addDumpCallback(this::dump, this);
         mPipInputConsumer = new PipInputConsumer(WindowManagerGlobal.getWindowManagerService(),
                 INPUT_CONSUMER_PIP, mMainExecutor);
         mPipTransitionController.registerPipTransitionCallback(this);
@@ -628,7 +636,8 @@
                 mPipTaskOrganizer.scheduleAnimateResizePip(
                         postChangeBounds, duration, null /* updateBoundsCallback */);
             } else {
-                mTouchHandler.getMotionHelper().movePip(postChangeBounds);
+                // Directly move PiP to its final destination bounds without animation.
+                mPipTaskOrganizer.scheduleFinishResizePip(postChangeBounds);
             }
         } else {
             updateDisplayLayout.run();
@@ -918,7 +927,7 @@
         return true;
     }
 
-    private void dump(PrintWriter pw) {
+    private void dump(PrintWriter pw, String prefix) {
         final String innerPrefix = "  ";
         pw.println(TAG);
         mMenuController.dump(pw, innerPrefix);
@@ -1006,18 +1015,6 @@
                 PipController.this.showPictureInPictureMenu();
             });
         }
-
-        @Override
-        public void dump(PrintWriter pw) {
-            try {
-                mMainExecutor.executeBlocking(() -> {
-                    PipController.this.dump(pw);
-                });
-            } catch (InterruptedException e) {
-                ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
-                        "%s: Failed to dump PipController in 2s", TAG);
-            }
-        }
     }
 
     /**
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/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
index 3d1a7e9..7b42350 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java
@@ -44,6 +44,7 @@
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.SplitBounds;
@@ -62,6 +63,7 @@
     private static final String TAG = RecentTasksController.class.getSimpleName();
 
     private final Context mContext;
+    private final ShellCommandHandler mShellCommandHandler;
     private final ShellExecutor mMainExecutor;
     private final TaskStackListenerImpl mTaskStackListener;
     private final RecentTasks mImpl = new RecentTasksImpl();
@@ -87,20 +89,24 @@
     public static RecentTasksController create(
             Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             TaskStackListenerImpl taskStackListener,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
         if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
             return null;
         }
-        return new RecentTasksController(context, shellInit, taskStackListener, mainExecutor);
+        return new RecentTasksController(context, shellInit, shellCommandHandler, taskStackListener,
+                mainExecutor);
     }
 
     RecentTasksController(Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             TaskStackListenerImpl taskStackListener,
             ShellExecutor mainExecutor) {
         mContext = context;
+        mShellCommandHandler = shellCommandHandler;
         mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
         mTaskStackListener = taskStackListener;
         mMainExecutor = mainExecutor;
@@ -112,6 +118,7 @@
     }
 
     private void onInit() {
+        mShellCommandHandler.addDumpCallback(this::dump, this);
         mTaskStackListener.addListener(this);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index a372acb..2117b69 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -84,6 +84,7 @@
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.splitscreen.SplitScreen.StageType;
 import com.android.wm.shell.sysui.KeyguardChangeListener;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
@@ -131,6 +132,7 @@
     @Retention(RetentionPolicy.SOURCE)
     @interface ExitReason{}
 
+    private final ShellCommandHandler mShellCommandHandler;
     private final ShellController mShellController;
     private final ShellTaskOrganizer mTaskOrganizer;
     private final SyncTransactionQueue mSyncQueue;
@@ -147,6 +149,7 @@
     private final SplitscreenEventLogger mLogger;
     private final IconProvider mIconProvider;
     private final Optional<RecentTasksController> mRecentTasksOptional;
+    private final SplitScreenShellCommandHandler mSplitScreenShellCommandHandler;
 
     private StageCoordinator mStageCoordinator;
     // Only used for the legacy recents animation from splitscreen to allow the tasks to be animated
@@ -155,6 +158,7 @@
 
     public SplitScreenController(Context context,
             ShellInit shellInit,
+            ShellCommandHandler shellCommandHandler,
             ShellController shellController,
             ShellTaskOrganizer shellTaskOrganizer,
             SyncTransactionQueue syncQueue,
@@ -168,6 +172,7 @@
             IconProvider iconProvider,
             Optional<RecentTasksController> recentTasks,
             ShellExecutor mainExecutor) {
+        mShellCommandHandler = shellCommandHandler;
         mShellController = shellController;
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
@@ -183,6 +188,7 @@
         mLogger = new SplitscreenEventLogger();
         mIconProvider = iconProvider;
         mRecentTasksOptional = recentTasks;
+        mSplitScreenShellCommandHandler = new SplitScreenShellCommandHandler(this);
         // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
         //                    override for this controller from the base module
         if (ActivityTaskManager.supportsSplitScreenMultiWindow(context)) {
@@ -200,6 +206,9 @@
      */
     @VisibleForTesting
     void onInit() {
+        mShellCommandHandler.addDumpCallback(this::dump, this);
+        mShellCommandHandler.addCommandCallback("splitscreen", mSplitScreenShellCommandHandler,
+                this);
         mShellController.addKeyguardChangeListener(this);
         if (mStageCoordinator == null) {
             // TODO: Multi-display
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java
new file mode 100644
index 0000000..681d964
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.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.wm.shell.splitscreen;
+
+import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+
+import com.android.wm.shell.sysui.ShellCommandHandler;
+
+import java.io.PrintWriter;
+
+/**
+ * Handles the shell commands for the SplitscreenController.
+ */
+public class SplitScreenShellCommandHandler implements
+        ShellCommandHandler.ShellCommandActionHandler {
+
+    private final SplitScreenController mController;
+
+    public SplitScreenShellCommandHandler(SplitScreenController controller) {
+        mController = controller;
+    }
+
+    @Override
+    public boolean onShellCommand(String[] args, PrintWriter pw) {
+        switch (args[0]) {
+            case "moveToSideStage":
+                return runMoveToSideStage(args, pw);
+            case "removeFromSideStage":
+                return runRemoveFromSideStage(args, pw);
+            case "setSideStagePosition":
+                return runSetSideStagePosition(args, pw);
+            default:
+                pw.println("Invalid command: " + args[0]);
+                return false;
+        }
+    }
+
+    private boolean runMoveToSideStage(String[] args, PrintWriter pw) {
+        if (args.length < 3) {
+            // First argument is the action name.
+            pw.println("Error: task id should be provided as arguments");
+            return false;
+        }
+        final int taskId = new Integer(args[1]);
+        final int sideStagePosition = args.length > 3
+                ? new Integer(args[2]) : SPLIT_POSITION_BOTTOM_OR_RIGHT;
+        mController.moveToSideStage(taskId, sideStagePosition);
+        return true;
+    }
+
+    private boolean runRemoveFromSideStage(String[] args, PrintWriter pw) {
+        if (args.length < 2) {
+            // First argument is the action name.
+            pw.println("Error: task id should be provided as arguments");
+            return false;
+        }
+        final int taskId = new Integer(args[1]);
+        mController.removeFromSideStage(taskId);
+        return true;
+    }
+
+    private boolean runSetSideStagePosition(String[] args, PrintWriter pw) {
+        if (args.length < 2) {
+            // First argument is the action name.
+            pw.println("Error: side stage position should be provided as arguments");
+            return false;
+        }
+        final int position = new Integer(args[1]);
+        mController.setSideStagePosition(position);
+        return true;
+    }
+
+    @Override
+    public void printShellCommandHelp(PrintWriter pw, String prefix) {
+        pw.println(prefix + "moveToSideStage <taskId> <SideStagePosition>");
+        pw.println(prefix + "  Move a task with given id in split-screen mode.");
+        pw.println(prefix + "removeFromSideStage <taskId>");
+        pw.println(prefix + "  Remove a task with given id in split-screen mode.");
+        pw.println(prefix + "setSideStagePosition <SideStagePosition>");
+        pw.println(prefix + "  Sets the position of the side-stage.");
+    }
+}
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/sysui/ShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java
index f4fc0c4..2e6ddc3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellCommandHandler.java
@@ -16,19 +16,14 @@
 
 package com.android.wm.shell.sysui;
 
-import static com.android.wm.shell.common.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT;
+import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT;
 
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
-import com.android.wm.shell.onehanded.OneHandedController;
-import com.android.wm.shell.pip.Pip;
-import com.android.wm.shell.recents.RecentTasksController;
-import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.internal.protolog.common.ProtoLog;
 
 import java.io.PrintWriter;
-import java.util.Optional;
+import java.util.Arrays;
+import java.util.TreeMap;
+import java.util.function.BiConsumer;
 
 /**
  * An entry point into the shell for dumping shell internal state and running adb commands.
@@ -38,52 +33,61 @@
 public final class ShellCommandHandler {
     private static final String TAG = ShellCommandHandler.class.getSimpleName();
 
-    private final Optional<SplitScreenController> mSplitScreenOptional;
-    private final Optional<Pip> mPipOptional;
-    private final Optional<OneHandedController> mOneHandedOptional;
-    private final Optional<HideDisplayCutoutController> mHideDisplayCutout;
-    private final Optional<RecentTasksController> mRecentTasks;
-    private final ShellTaskOrganizer mShellTaskOrganizer;
-    private final KidsModeTaskOrganizer mKidsModeTaskOrganizer;
+    // We're using a TreeMap to keep them sorted by command name
+    private final TreeMap<String, BiConsumer<PrintWriter, String>> mDumpables = new TreeMap<>();
+    private final TreeMap<String, ShellCommandActionHandler> mCommands = new TreeMap<>();
 
-    public ShellCommandHandler(
-            ShellController shellController,
-            ShellTaskOrganizer shellTaskOrganizer,
-            KidsModeTaskOrganizer kidsModeTaskOrganizer,
-            Optional<SplitScreenController> splitScreenOptional,
-            Optional<Pip> pipOptional,
-            Optional<OneHandedController> oneHandedOptional,
-            Optional<HideDisplayCutoutController> hideDisplayCutout,
-            Optional<RecentTasksController> recentTasks,
-            ShellExecutor mainExecutor) {
-        mShellTaskOrganizer = shellTaskOrganizer;
-        mKidsModeTaskOrganizer = kidsModeTaskOrganizer;
-        mRecentTasks = recentTasks;
-        mSplitScreenOptional = splitScreenOptional;
-        mPipOptional = pipOptional;
-        mOneHandedOptional = oneHandedOptional;
-        mHideDisplayCutout = hideDisplayCutout;
-        // TODO(238217847): To be removed once the command handler dependencies are inverted
-        shellController.setShellCommandHandler(this);
+    public interface ShellCommandActionHandler {
+        /**
+         * Handles the given command.
+         *
+         * @param args the arguments starting with the action name, then the action arguments
+         * @param pw the write to print output to
+         */
+        boolean onShellCommand(String[] args, PrintWriter pw);
+
+        /**
+         * Prints the help for this class of commands.  Implementations do not need to print the
+         * command class.
+         */
+        void printShellCommandHelp(PrintWriter pw, String prefix);
+    }
+
+
+    /**
+     * Adds a callback to run when the Shell is being dumped.
+     *
+     * @param callback the callback to be made when Shell is dumped, takes the print writer and
+     *                 a prefix
+     * @param instance used for debugging only
+     */
+    public <T> void addDumpCallback(BiConsumer<PrintWriter, String> callback, T instance) {
+        mDumpables.put(instance.getClass().getSimpleName(), callback);
+        ProtoLog.v(WM_SHELL_INIT, "Adding dump callback for %s",
+                instance.getClass().getSimpleName());
+    }
+
+    /**
+     * Adds an action callback to be invoked when the user runs that particular command from adb.
+     *
+     * @param commandClass the top level class of command to invoke
+     * @param actions the interface to callback when an action of this class is invoked
+     * @param instance used for debugging only
+     */
+    public <T> void addCommandCallback(String commandClass, ShellCommandActionHandler actions,
+            T instance) {
+        mCommands.put(commandClass, actions);
+        ProtoLog.v(WM_SHELL_INIT, "Adding command class %s for %s", commandClass,
+                instance.getClass().getSimpleName());
     }
 
     /** Dumps WM Shell internal state. */
     public void dump(PrintWriter pw) {
-        mShellTaskOrganizer.dump(pw, "");
-        pw.println();
-        pw.println();
-        mPipOptional.ifPresent(pip -> pip.dump(pw));
-        mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw));
-        mHideDisplayCutout.ifPresent(hideDisplayCutout -> hideDisplayCutout.dump(pw));
-        pw.println();
-        pw.println();
-        mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw, ""));
-        pw.println();
-        pw.println();
-        mRecentTasks.ifPresent(recentTasks -> recentTasks.dump(pw, ""));
-        pw.println();
-        pw.println();
-        mKidsModeTaskOrganizer.dump(pw, "");
+        for (String key : mDumpables.keySet()) {
+            final BiConsumer<PrintWriter, String> r = mDumpables.get(key);
+            r.accept(pw, "");
+            pw.println();
+        }
     }
 
 
@@ -93,72 +97,32 @@
             // Argument at position 0 is "WMShell".
             return false;
         }
-        switch (args[1]) {
-            case "moveToSideStage":
-                return runMoveToSideStage(args, pw);
-            case "removeFromSideStage":
-                return runRemoveFromSideStage(args, pw);
-            case "setSideStagePosition":
-                return runSetSideStagePosition(args, pw);
-            case "help":
-                return runHelp(pw);
-            default:
-                return false;
-        }
-    }
 
-    private boolean runMoveToSideStage(String[] args, PrintWriter pw) {
-        if (args.length < 3) {
-            // First arguments are "WMShell" and command name.
-            pw.println("Error: task id should be provided as arguments");
+        final String cmdClass = args[1];
+        if (cmdClass.toLowerCase().equals("help")) {
+            return runHelp(pw);
+        }
+        if (!mCommands.containsKey(cmdClass)) {
             return false;
         }
-        final int taskId = new Integer(args[2]);
-        final int sideStagePosition = args.length > 3
-                ? new Integer(args[3]) : SPLIT_POSITION_BOTTOM_OR_RIGHT;
-        mSplitScreenOptional.ifPresent(split -> split.moveToSideStage(taskId, sideStagePosition));
-        return true;
-    }
 
-    private boolean runRemoveFromSideStage(String[] args, PrintWriter pw) {
-        if (args.length < 3) {
-            // First arguments are "WMShell" and command name.
-            pw.println("Error: task id should be provided as arguments");
-            return false;
-        }
-        final int taskId = new Integer(args[2]);
-        mSplitScreenOptional.ifPresent(split -> split.removeFromSideStage(taskId));
-        return true;
-    }
-
-    private boolean runSetSideStagePosition(String[] args, PrintWriter pw) {
-        if (args.length < 3) {
-            // First arguments are "WMShell" and command name.
-            pw.println("Error: side stage position should be provided as arguments");
-            return false;
-        }
-        final int position = new Integer(args[2]);
-        mSplitScreenOptional.ifPresent(split -> split.setSideStagePosition(position));
+        // Only pass the actions onwards as arguments to the callback
+        final ShellCommandActionHandler actions = mCommands.get(args[1]);
+        final String[] cmdClassArgs = Arrays.copyOfRange(args, 2, args.length);
+        actions.onShellCommand(cmdClassArgs, pw);
         return true;
     }
 
     private boolean runHelp(PrintWriter pw) {
         pw.println("Window Manager Shell commands:");
+        for (String commandClass : mCommands.keySet()) {
+            pw.println("  " + commandClass);
+            mCommands.get(commandClass).printShellCommandHelp(pw, "    ");
+        }
         pw.println("  help");
         pw.println("      Print this help text.");
         pw.println("  <no arguments provided>");
-        pw.println("    Dump Window Manager Shell internal state");
-        pw.println("  pair <taskId1> <taskId2>");
-        pw.println("  unpair <taskId>");
-        pw.println("    Pairs/unpairs tasks with given ids.");
-        pw.println("  moveToSideStage <taskId> <SideStagePosition>");
-        pw.println("    Move a task with given id in split-screen mode.");
-        pw.println("  removeFromSideStage <taskId>");
-        pw.println("    Remove a task with given id in split-screen mode.");
-        pw.println("  setSideStageOutline <true/false>");
-        pw.println("    Enable/Disable outline on the side-stage.");
-        pw.println("  setSideStagePosition <SideStagePosition>");
-        pw.println("    Sets the position of the side-stage.");
+        pw.println("    Dump all Window Manager Shell internal state");
         return true;
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
index f1f317f..52ffb46 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellController.java
@@ -45,11 +45,10 @@
     private static final String TAG = ShellController.class.getSimpleName();
 
     private final ShellInit mShellInit;
+    private final ShellCommandHandler mShellCommandHandler;
     private final ShellExecutor mMainExecutor;
     private final ShellInterfaceImpl mImpl = new ShellInterfaceImpl();
 
-    private ShellCommandHandler mShellCommandHandler;
-
     private final CopyOnWriteArrayList<ConfigurationChangeListener> mConfigChangeListeners =
             new CopyOnWriteArrayList<>();
     private final CopyOnWriteArrayList<KeyguardChangeListener> mKeyguardChangeListeners =
@@ -57,8 +56,10 @@
     private Configuration mLastConfiguration;
 
 
-    public ShellController(ShellInit shellInit, ShellExecutor mainExecutor) {
+    public ShellController(ShellInit shellInit, ShellCommandHandler shellCommandHandler,
+            ShellExecutor mainExecutor) {
         mShellInit = shellInit;
+        mShellCommandHandler = shellCommandHandler;
         mMainExecutor = mainExecutor;
     }
 
@@ -70,15 +71,6 @@
     }
 
     /**
-     * Sets the command handler to call back to.
-     * TODO(238217847): This is only exposed this way until we can remove the dependencies from the
-     *                  command handler to other classes.
-     */
-    public void setShellCommandHandler(ShellCommandHandler shellCommandHandler) {
-        mShellCommandHandler = shellCommandHandler;
-    }
-
-    /**
      * Adds a new configuration listener. The configuration change callbacks are not made in any
      * particular order.
      */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
index c250e03..ac52235 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/sysui/ShellInit.java
@@ -52,6 +52,9 @@
      * Adds a callback to the ordered list of callbacks be made when Shell is first started.  This
      * can be used in class constructors when dagger is used to ensure that the initialization order
      * matches the dependency order.
+     *
+     * @param r the callback to be made when Shell is initialized
+     * @param instance used for debugging only
      */
     public <T extends Object> void addInitCallback(Runnable r, T instance) {
         if (mHasInitialized) {
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/ShellTaskOrganizerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
index 7517e8a..f865649 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTaskOrganizerTests.java
@@ -59,6 +59,7 @@
 
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
@@ -85,10 +86,12 @@
     @Mock
     private CompatUIController mCompatUI;
     @Mock
-    private ShellInit mShellInit;
+    private ShellExecutor mTestExecutor;
+    @Mock
+    private ShellCommandHandler mShellCommandHandler;
 
-    ShellTaskOrganizer mOrganizer;
-    private final ShellExecutor mTestExecutor = mock(ShellExecutor.class);
+    private ShellTaskOrganizer mOrganizer;
+    private ShellInit mShellInit;
 
     private class TrackingTaskListener implements ShellTaskOrganizer.TaskListener {
         final ArrayList<RunningTaskInfo> appeared = new ArrayList<>();
@@ -132,8 +135,11 @@
             doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList())
                     .when(mTaskOrganizerController).registerTaskOrganizer(any());
         } catch (RemoteException e) {}
-        mOrganizer = spy(new ShellTaskOrganizer(mShellInit, mTaskOrganizerController,
-                mCompatUI, Optional.empty(), Optional.empty(), mTestExecutor));
+        mShellInit = spy(new ShellInit(mTestExecutor));
+        mOrganizer = spy(new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
+                mTaskOrganizerController, mCompatUI, Optional.empty(), Optional.empty(),
+                mTestExecutor));
+        mShellInit.init();
     }
 
     @Test
@@ -142,9 +148,12 @@
     }
 
     @Test
-    public void testRegisterOrganizer_sendRegisterTaskOrganizer() throws RemoteException {
-        mOrganizer.registerOrganizer();
+    public void instantiate_addDumpCallback() {
+        verify(mShellCommandHandler, times(1)).addDumpCallback(any(), any());
+    }
 
+    @Test
+    public void testInit_sendRegisterTaskOrganizer() throws RemoteException {
         verify(mTaskOrganizerController).registerTaskOrganizer(any(ITaskOrganizer.class));
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index ba81602..5b3b8fd 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -61,6 +62,7 @@
 import com.android.internal.util.test.FakeSettingsProvider;
 import com.android.wm.shell.ShellTestCase;
 import com.android.wm.shell.TestShellExecutor;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -81,6 +83,7 @@
 
     private static final String ANIMATION_ENABLED = "1";
     private final TestShellExecutor mShellExecutor = new TestShellExecutor();
+    private ShellInit mShellInit;
 
     @Rule
     public TestableContext mContext =
@@ -110,10 +113,12 @@
         Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION,
                 ANIMATION_ENABLED);
         mTestableLooper = TestableLooper.get(this);
-        mController = new BackAnimationController(
+        mShellInit = spy(new ShellInit(mShellExecutor));
+        mController = new BackAnimationController(mShellInit,
                 mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
                 mActivityTaskManager, mContext,
                 mContentResolver);
+        mShellInit.init();
         mEventTime = 0;
         mShellExecutor.flushAll();
     }
@@ -160,6 +165,11 @@
     }
 
     @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
     @Ignore("b/207481538")
     public void crossActivity_screenshotAttachedAndVisible() {
         SurfaceControl screenshotSurface = new SurfaceControl();
@@ -233,10 +243,12 @@
     public void animationDisabledFromSettings() throws RemoteException {
         // Toggle the setting off
         Settings.Global.putString(mContentResolver, Settings.Global.ENABLE_BACK_ANIMATION, "0");
-        mController = new BackAnimationController(
+        ShellInit shellInit = new ShellInit(mShellExecutor);
+        mController = new BackAnimationController(shellInit,
                 mShellExecutor, new Handler(mTestableLooper.getLooper()), mTransaction,
                 mActivityTaskManager, mContext,
                 mContentResolver);
+        shellInit.init();
         mController.setBackToLauncherCallback(mIOnBackInvokedCallback);
 
         RemoteAnimationTarget animationTarget = createAnimationTarget();
@@ -272,9 +284,14 @@
         // the previous transition is finished.
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         verifyNoMoreInteractions(mIOnBackInvokedCallback);
+        mController.onBackToLauncherAnimationFinished();
+
+        // Verify that more events from a rejected swipe cannot start animation.
+        doMotionEvent(MotionEvent.ACTION_MOVE, 100);
+        doMotionEvent(MotionEvent.ACTION_UP, 0);
+        verifyNoMoreInteractions(mIOnBackInvokedCallback);
 
         // Verify that we start accepting gestures again once transition finishes.
-        mController.onBackToLauncherAnimationFinished();
         doMotionEvent(MotionEvent.ACTION_DOWN, 0);
         doMotionEvent(MotionEvent.ACTION_MOVE, 100);
         verify(mIOnBackInvokedCallback).onBackStarted();
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/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
index 828c13e..6292130 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java
@@ -29,6 +29,7 @@
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -54,6 +55,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.compatui.letterboxedu.LetterboxEduWindowManager;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -79,6 +81,7 @@
     private static final int TASK_ID = 12;
 
     private CompatUIController mController;
+    private ShellInit mShellInit;
     private @Mock ShellController mMockShellController;
     private @Mock DisplayController mMockDisplayController;
     private @Mock DisplayInsetsController mMockDisplayInsetsController;
@@ -107,9 +110,10 @@
         doReturn(TASK_ID).when(mMockLetterboxEduLayout).getTaskId();
         doReturn(true).when(mMockLetterboxEduLayout).createLayout(anyBoolean());
         doReturn(true).when(mMockLetterboxEduLayout).updateCompatInfo(any(), any(), anyBoolean());
-        mController = new CompatUIController(mContext, mMockShellController, mMockDisplayController,
-                mMockDisplayInsetsController, mMockImeController, mMockSyncQueue, mMockExecutor,
-                mMockTransitionsLazy) {
+        mShellInit = spy(new ShellInit(mMockExecutor));
+        mController = new CompatUIController(mContext, mShellInit, mMockShellController,
+                mMockDisplayController, mMockDisplayInsetsController, mMockImeController,
+                mMockSyncQueue, mMockExecutor, mMockTransitionsLazy) {
             @Override
             CompatUIWindowManager createCompatUiWindowManager(Context context, TaskInfo taskInfo,
                     ShellTaskOrganizer.TaskListener taskListener) {
@@ -122,10 +126,16 @@
                 return mMockLetterboxEduLayout;
             }
         };
+        mShellInit.init();
         spyOn(mController);
     }
 
     @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
     public void instantiateController_registerKeyguardChangeListener() {
         verify(mMockShellController, times(1)).addKeyguardChangeListener(any());
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java
index dcc504a..6c301bb 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/hidedisplaycutout/HideDisplayCutoutControllerTest.java
@@ -17,7 +17,9 @@
 package com.android.wm.shell.hidedisplaycutout;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
@@ -29,7 +31,10 @@
 import androidx.test.platform.app.InstrumentationRegistry;
 
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -45,17 +50,32 @@
             InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
 
     @Mock
+    private ShellCommandHandler mShellCommandHandler;
+    @Mock
     private ShellController mShellController;
     @Mock
     private HideDisplayCutoutOrganizer mMockDisplayAreaOrganizer;
 
     private HideDisplayCutoutController mHideDisplayCutoutController;
+    private ShellInit mShellInit;
 
     @Before
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mHideDisplayCutoutController = new HideDisplayCutoutController(
-                mContext, mShellController, mMockDisplayAreaOrganizer);
+        mShellInit = spy(new ShellInit(mock(ShellExecutor.class)));
+        mHideDisplayCutoutController = new HideDisplayCutoutController(mContext, mShellInit,
+                mShellCommandHandler, mShellController, mMockDisplayAreaOrganizer);
+        mShellInit.init();
+    }
+
+    @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
+    public void instantiateController_registerDumpCallback() {
+        verify(mShellCommandHandler, times(1)).addDumpCallback(any(), any());
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
index a919ad0..ecfb427 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizerTest.java
@@ -49,6 +49,7 @@
 import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
@@ -74,6 +75,7 @@
     @Mock private WindowContainerTransaction mTransaction;
     @Mock private KidsModeSettingsObserver mObserver;
     @Mock private ShellInit mShellInit;
+    @Mock private ShellCommandHandler mShellCommandHandler;
     @Mock private DisplayInsetsController mDisplayInsetsController;
 
     KidsModeTaskOrganizer mOrganizer;
@@ -87,14 +89,20 @@
         } catch (RemoteException e) {
         }
         // NOTE: KidsModeTaskOrganizer should have a null CompatUIController.
-        mOrganizer = spy(new KidsModeTaskOrganizer(mContext, mTaskOrganizerController,
-                mSyncTransactionQueue, mDisplayController, mDisplayInsetsController,
-                Optional.empty(), Optional.empty(), mObserver, mTestExecutor, mHandler));
+        mOrganizer = spy(new KidsModeTaskOrganizer(mContext, mShellInit, mShellCommandHandler,
+                mTaskOrganizerController, mSyncTransactionQueue, mDisplayController,
+                mDisplayInsetsController, Optional.empty(), Optional.empty(), mObserver,
+                mTestExecutor, mHandler));
         doReturn(mTransaction).when(mOrganizer).getWindowContainerTransaction();
         doReturn(new InsetsState()).when(mDisplayController).getInsetsState(DEFAULT_DISPLAY);
     }
 
     @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
     public void testKidsModeOn() {
         doReturn(true).when(mObserver).isEnabled();
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
index dbf93ae..90645ce 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedControllerTest.java
@@ -36,7 +36,6 @@
 
 import android.graphics.Rect;
 import android.os.Handler;
-import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
 import android.util.ArrayMap;
 import android.view.Display;
@@ -49,7 +48,9 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -61,18 +62,20 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class OneHandedControllerTest extends OneHandedTestCase {
-    private int mCurrentUser = UserHandle.myUserId();
 
     Display mDisplay;
     OneHandedAccessibilityUtil mOneHandedAccessibilityUtil;
     OneHandedController mSpiedOneHandedController;
     OneHandedTimeoutHandler mSpiedTimeoutHandler;
     OneHandedState mSpiedTransitionState;
+    ShellInit mShellInit;
 
     @Mock
+    ShellCommandHandler mMockShellCommandHandler;
+    @Mock
     ShellController mMockShellController;
     @Mock
-    DisplayLayout mDisplayLayout;
+    DisplayLayout mMockDisplayLayout;
     @Mock
     DisplayController mMockDisplayController;
     @Mock
@@ -102,7 +105,7 @@
     public void setUp() {
         MockitoAnnotations.initMocks(this);
         mDisplay = mContext.getDisplay();
-        mDisplayLayout = Mockito.mock(DisplayLayout.class);
+        mMockDisplayLayout = Mockito.mock(DisplayLayout.class);
         mSpiedTimeoutHandler = spy(new OneHandedTimeoutHandler(mMockShellMainExecutor));
         mSpiedTransitionState = spy(new OneHandedState());
 
@@ -122,11 +125,14 @@
 
         when(mMockDisplayAreaOrganizer.getLastDisplayBounds()).thenReturn(
                 new Rect(0, 0, 1080, 2400));
-        when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(mDisplayLayout);
+        when(mMockDisplayAreaOrganizer.getDisplayLayout()).thenReturn(mMockDisplayLayout);
 
+        mShellInit = spy(new ShellInit(mMockShellMainExecutor));
         mOneHandedAccessibilityUtil = new OneHandedAccessibilityUtil(mContext);
         mSpiedOneHandedController = spy(new OneHandedController(
                 mContext,
+                mShellInit,
+                mMockShellCommandHandler,
                 mMockShellController,
                 mMockDisplayController,
                 mMockDisplayAreaOrganizer,
@@ -141,6 +147,17 @@
                 mMockShellMainExecutor,
                 mMockShellMainHandler)
         );
+        mShellInit.init();
+    }
+
+    @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
+    public void instantiateController_registerDumpCallback() {
+        verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), any());
     }
 
     @Test
@@ -308,9 +325,9 @@
 
     @Test
     public void testRotation90CanNotStartOneHanded() {
-        mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_90);
+        mMockDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_90);
         mSpiedTransitionState.setState(STATE_NONE);
-        when(mDisplayLayout.isLandscape()).thenReturn(true);
+        when(mMockDisplayLayout.isLandscape()).thenReturn(true);
         mSpiedOneHandedController.setOneHandedEnabled(true);
         mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */);
         mSpiedOneHandedController.startOneHanded();
@@ -320,10 +337,10 @@
 
     @Test
     public void testRotation180CanStartOneHanded() {
-        mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_180);
+        mMockDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_180);
         mSpiedTransitionState.setState(STATE_NONE);
         when(mMockDisplayAreaOrganizer.isReady()).thenReturn(true);
-        when(mDisplayLayout.isLandscape()).thenReturn(false);
+        when(mMockDisplayLayout.isLandscape()).thenReturn(false);
         mSpiedOneHandedController.setOneHandedEnabled(true);
         mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */);
         mSpiedOneHandedController.startOneHanded();
@@ -333,9 +350,9 @@
 
     @Test
     public void testRotation270CanNotStartOneHanded() {
-        mDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_270);
+        mMockDisplayLayout.rotateTo(mContext.getResources(), Surface.ROTATION_270);
         mSpiedTransitionState.setState(STATE_NONE);
-        when(mDisplayLayout.isLandscape()).thenReturn(true);
+        when(mMockDisplayLayout.isLandscape()).thenReturn(true);
         mSpiedOneHandedController.setOneHandedEnabled(true);
         mSpiedOneHandedController.setLockedDisabled(false /* locked */, false /* enabled */);
         mSpiedOneHandedController.startOneHanded();
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java
index e6a8220..a39bdf0 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/onehanded/OneHandedStateTest.java
@@ -41,7 +41,9 @@
 import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -61,6 +63,10 @@
     OneHandedState mSpiedState;
 
     @Mock
+    ShellInit mMockShellInit;
+    @Mock
+    ShellCommandHandler mMockShellCommandHandler;
+    @Mock
     ShellController mMockShellController;
     @Mock
     DisplayController mMockDisplayController;
@@ -111,6 +117,8 @@
         mOneHandedAccessibilityUtil = new OneHandedAccessibilityUtil(mContext);
         mSpiedOneHandedController = spy(new OneHandedController(
                 mContext,
+                mMockShellInit,
+                mMockShellCommandHandler,
                 mMockShellController,
                 mMockDisplayController,
                 mMockDisplayAreaOrganizer,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
index f192514..9ed8d84 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java
@@ -55,7 +55,9 @@
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipTransitionState;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -74,7 +76,9 @@
 @TestableLooper.RunWithLooper
 public class PipControllerTest extends ShellTestCase {
     private PipController mPipController;
+    private ShellInit mShellInit;
 
+    @Mock private ShellCommandHandler mMockShellCommandHandler;
     @Mock private ShellController mMockShellController;
     @Mock private DisplayController mMockDisplayController;
     @Mock private PhonePipMenuController mMockPhonePipMenuController;
@@ -105,19 +109,31 @@
             ((Runnable) invocation.getArgument(0)).run();
             return null;
         }).when(mMockExecutor).execute(any());
-        mPipController = new PipController(mContext, mMockShellController, mMockDisplayController,
-                mMockPipAppOpsListener, mMockPipBoundsAlgorithm,
-                mMockPipKeepClearAlgorithm,
+        mShellInit = spy(new ShellInit(mMockExecutor));
+        mPipController = new PipController(mContext, mShellInit, mMockShellCommandHandler,
+                mMockShellController, mMockDisplayController, mMockPipAppOpsListener,
+                mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
                 mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
                 mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
                 mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
                 mMockTaskStackListener, mPipParamsChangedForwarder,
                 mMockOneHandedController, mMockExecutor);
+        mShellInit.init();
         when(mMockPipBoundsAlgorithm.getSnapAlgorithm()).thenReturn(mMockPipSnapAlgorithm);
         when(mMockPipTouchHandler.getMotionHelper()).thenReturn(mMockPipMotionHelper);
     }
 
     @Test
+    public void instantiatePipController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
+    public void instantiateController_registerDumpCallback() {
+        verify(mMockShellCommandHandler, times(1)).addDumpCallback(any(), any());
+    }
+
+    @Test
     public void instantiatePipController_registerConfigChangeListener() {
         verify(mMockShellController, times(1)).addConfigurationChangeListener(any());
     }
@@ -149,9 +165,10 @@
         when(mockPackageManager.hasSystemFeature(FEATURE_PICTURE_IN_PICTURE)).thenReturn(false);
         when(spyContext.getPackageManager()).thenReturn(mockPackageManager);
 
-        assertNull(PipController.create(spyContext, mMockShellController, mMockDisplayController,
-                mMockPipAppOpsListener, mMockPipBoundsAlgorithm,
-                mMockPipKeepClearAlgorithm,
+        ShellInit shellInit = new ShellInit(mMockExecutor);
+        assertNull(PipController.create(spyContext, shellInit, mMockShellCommandHandler,
+                mMockShellController, mMockDisplayController, mMockPipAppOpsListener,
+                mMockPipBoundsAlgorithm, mMockPipKeepClearAlgorithm,
                 mMockPipBoundsState, mMockPipMotionHelper, mMockPipMediaController,
                 mMockPhonePipMenuController, mMockPipTaskOrganizer, mMockPipTransitionState,
                 mMockPipTouchHandler, mMockPipTransitionController, mMockWindowManagerShellWrapper,
@@ -217,7 +234,7 @@
         mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged(
                 displayId, new Configuration());
 
-        verify(mMockPipMotionHelper).movePip(any(Rect.class));
+        verify(mMockPipTaskOrganizer).scheduleFinishResizePip(any(Rect.class));
     }
 
     @Test
@@ -233,7 +250,7 @@
         mPipController.mDisplaysChangedListener.onDisplayConfigurationChanged(
                 displayId, new Configuration());
 
-        verify(mMockPipMotionHelper, never()).movePip(any(Rect.class));
+        verify(mMockPipTaskOrganizer, never()).scheduleFinishResizePip(any(Rect.class));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
index d406a4e..81bb609 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java
@@ -23,7 +23,9 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.isA;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
@@ -48,6 +50,7 @@
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.SplitBounds;
@@ -73,21 +76,35 @@
     @Mock
     private TaskStackListenerImpl mTaskStackListener;
     @Mock
-    private ShellInit mShellInit;
+    private ShellCommandHandler mShellCommandHandler;
 
     private ShellTaskOrganizer mShellTaskOrganizer;
     private RecentTasksController mRecentTasksController;
+    private ShellInit mShellInit;
     private ShellExecutor mMainExecutor;
 
     @Before
     public void setUp() {
         mMainExecutor = new TestShellExecutor();
         when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
+        mShellInit = spy(new ShellInit(mMainExecutor));
         mRecentTasksController = spy(new RecentTasksController(mContext, mShellInit,
-                mTaskStackListener, mMainExecutor));
-        mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit,
+                mShellCommandHandler, mTaskStackListener, mMainExecutor));
+        mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit, mShellCommandHandler,
                 null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
                 mMainExecutor);
+        mShellInit.init();
+    }
+
+    @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), isA(RecentTasksController.class));
+    }
+
+    @Test
+    public void instantiateController_addDumpCallback() {
+        verify(mShellCommandHandler, times(1)).addDumpCallback(any(),
+                isA(RecentTasksController.class));
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
index c7c78d3..5a68361 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java
@@ -27,6 +27,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -54,6 +55,7 @@
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.sysui.ShellCommandHandler;
 import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
@@ -73,8 +75,9 @@
 @RunWith(AndroidJUnit4.class)
 public class SplitScreenControllerTests extends ShellTestCase {
 
-    @Mock ShellController mShellController;
     @Mock ShellInit mShellInit;
+    @Mock ShellController mShellController;
+    @Mock ShellCommandHandler mShellCommandHandler;
     @Mock ShellTaskOrganizer mTaskOrganizer;
     @Mock SyncTransactionQueue mSyncQueue;
     @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
@@ -94,9 +97,10 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
         mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
-                mShellController, mTaskOrganizer, mSyncQueue, mRootTDAOrganizer, mDisplayController,
-                mDisplayImeController, mDisplayInsetsController, mDragAndDropController,
-                mTransitions, mTransactionPool, mIconProvider, mRecentTasks, mMainExecutor));
+                mShellCommandHandler, mShellController, mTaskOrganizer, mSyncQueue,
+                mRootTDAOrganizer, mDisplayController, mDisplayImeController,
+                mDisplayInsetsController, mDragAndDropController, mTransitions, mTransactionPool,
+                mIconProvider, mRecentTasks, mMainExecutor));
     }
 
     @Test
@@ -105,7 +109,24 @@
     }
 
     @Test
+    public void instantiateController_registerDumpCallback() {
+        doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
+        when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
+        mSplitScreenController.onInit();
+        verify(mShellCommandHandler, times(1)).addDumpCallback(any(), any());
+    }
+
+    @Test
+    public void instantiateController_registerCommandCallback() {
+        doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
+        when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
+        mSplitScreenController.onInit();
+        verify(mShellCommandHandler, times(1)).addCommandCallback(eq("splitscreen"), any(), any());
+    }
+
+    @Test
     public void testControllerRegistersKeyguardChangeListener() {
+        doReturn(mMainExecutor).when(mTaskOrganizer).getExecutor();
         when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
         mSplitScreenController.onInit();
         verify(mShellController, times(1)).addKeyguardChangeListener(any());
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
index 02311ba..39e58ff 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/sysui/ShellControllerTest.java
@@ -45,6 +45,8 @@
     @Mock
     private ShellInit mShellInit;
     @Mock
+    private ShellCommandHandler mShellCommandHandler;
+    @Mock
     private ShellExecutor mExecutor;
 
     private ShellController mController;
@@ -56,7 +58,7 @@
         MockitoAnnotations.initMocks(this);
         mKeyguardChangeListener = new TestKeyguardChangeListener();
         mConfigChangeListener = new TestConfigurationChangeListener();
-        mController = new ShellController(mShellInit, mExecutor);
+        mController = new ShellController(mShellInit, mShellCommandHandler, mExecutor);
         mController.onConfigurationChanged(getConfigurationCopy());
     }
 
diff --git a/libs/hwui/jni/android_graphics_Canvas.cpp b/libs/hwui/jni/android_graphics_Canvas.cpp
index 0ef80ee..132234b 100644
--- a/libs/hwui/jni/android_graphics_Canvas.cpp
+++ b/libs/hwui/jni/android_graphics_Canvas.cpp
@@ -407,14 +407,28 @@
         indices = (const uint16_t*)(indexA.ptr() + indexIndex);
     }
 
-    SkVertices::VertexMode mode = static_cast<SkVertices::VertexMode>(modeHandle);
+    SkVertices::VertexMode vertexMode = static_cast<SkVertices::VertexMode>(modeHandle);
     const Paint* paint = reinterpret_cast<Paint*>(paintHandle);
-    get_canvas(canvasHandle)->drawVertices(SkVertices::MakeCopy(mode, vertexCount,
-                                           reinterpret_cast<const SkPoint*>(verts),
-                                           reinterpret_cast<const SkPoint*>(texs),
-                                           reinterpret_cast<const SkColor*>(colors),
-                                           indexCount, indices).get(),
-                                           SkBlendMode::kModulate, *paint);
+
+    // Preserve legacy Skia behavior: ignore the shader if there are no texs set.
+    Paint noShaderPaint;
+    if (jtexs == NULL) {
+        noShaderPaint = Paint(*paint);
+        noShaderPaint.setShader(nullptr);
+        paint = &noShaderPaint;
+    }
+    // Since https://skia-review.googlesource.com/c/skia/+/473676, Skia will blend paint and vertex
+    // colors when no shader is provided. This ternary uses kDst to mimic the old behavior of
+    // ignoring the paint and using the vertex colors directly when no shader is provided.
+    SkBlendMode blendMode = paint->getShader() ? SkBlendMode::kModulate : SkBlendMode::kDst;
+
+    get_canvas(canvasHandle)
+            ->drawVertices(SkVertices::MakeCopy(
+                                   vertexMode, vertexCount, reinterpret_cast<const SkPoint*>(verts),
+                                   reinterpret_cast<const SkPoint*>(texs),
+                                   reinterpret_cast<const SkColor*>(colors), indexCount, indices)
+                                   .get(),
+                           blendMode, *paint);
 }
 
 static void drawNinePatch(JNIEnv* env, jobject, jlong canvasHandle, jlong bitmapHandle,
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
index 01e3de2..898935f 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher.xml
@@ -35,7 +35,7 @@
 
     <!-- need to keep this outer view in order to have a correctly sized anchor
          for the dropdown menu, as well as dropdown background in the right place -->
-    <LinearLayout
+    <com.android.keyguard.KeyguardUserSwitcherAnchor
         android:id="@+id/user_switcher_anchor"
         android:orientation="horizontal"
         android:layout_height="wrap_content"
@@ -48,7 +48,7 @@
           android:textDirection="locale"
           android:layout_width="@dimen/bouncer_user_switcher_width"
           android:layout_height="wrap_content" />
-    </LinearLayout>>
+    </com.android.keyguard.KeyguardUserSwitcherAnchor>
 
 </LinearLayout>
 
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/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 9c2542c..1bf3037 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -887,7 +887,8 @@
     <!-- Accessibility label for the button that opens the user switcher. -->
     <string name="accessibility_multi_user_switch_switcher">Switch user</string>
 
-    <!-- Accessibility label for the button that opens the user switcher and announces the current user. -->
+    <!-- Accessibility role description for the element that opens the user switcher list. -->
+    <string name="accessibility_multi_user_list_switcher">pulldown menu</string>
 
     <!-- Accessibility label for the user icon on the lock screen. -->
 
diff --git a/packages/SystemUI/res/xml/qqs_header.xml b/packages/SystemUI/res/xml/qqs_header.xml
index ee0c4fb6..a82684d03 100644
--- a/packages/SystemUI/res/xml/qqs_header.xml
+++ b/packages/SystemUI/res/xml/qqs_header.xml
@@ -43,7 +43,7 @@
         android:id="@+id/date">
         <Layout
             android:layout_width="0dp"
-            android:layout_height="0dp"
+            android:layout_height="@dimen/qs_header_non_clickable_element_height"
             app:layout_constrainedWidth="true"
             app:layout_constraintStart_toEndOf="@id/clock"
             app:layout_constraintEnd_toStartOf="@id/barrier"
@@ -61,7 +61,7 @@
             app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/date"
             app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
-            app:layout_constraintTop_toTopOf="@id/date"
+            app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintHorizontal_bias="1"
             />
@@ -76,7 +76,7 @@
             app:layout_constraintHeight_min="@dimen/qs_header_non_clickable_element_height"
             app:layout_constraintStart_toEndOf="@id/statusIcons"
             app:layout_constraintEnd_toEndOf="@id/end_guide"
-            app:layout_constraintTop_toTopOf="@id/date"
+            app:layout_constraintTop_toTopOf="parent"
             app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintHorizontal_bias="1"
             />
@@ -100,8 +100,8 @@
             android:layout_height="0dp"
             app:layout_constraintStart_toEndOf="@id/date"
             app:layout_constraintEnd_toEndOf="@id/end_guide"
-            app:layout_constraintTop_toTopOf="@id/date"
-            app:layout_constraintBottom_toBottomOf="@id/date"
+            app:layout_constraintTop_toTopOf="parent"
+            app:layout_constraintBottom_toBottomOf="parent"
             app:layout_constraintHorizontal_bias="1"
         />
     </Constraint>
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/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index 0149751..4613e8b 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -100,4 +100,14 @@
      * Sent when the desired dark intensity of the nav buttons has changed
      */
     void onNavButtonsDarkIntensityChanged(float darkIntensity) = 22;
+
+     /**
+      * Sent when screen started turning on.
+      */
+     void onScreenTurningOn() = 23;
+
+     /**
+      * Sent when screen started turning off.
+      */
+     void onScreenTurningOff() = 24;
 }
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/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index f697e25..3517d22 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -1129,11 +1129,13 @@
                 Log.e(TAG, "Current user in user switcher is null.");
                 return;
             }
+            final String currentUserName = mUserSwitcherController.getCurrentUserName();
             Drawable userIcon = findUserIcon(currentUser.info.id);
             ((ImageView) mView.findViewById(R.id.user_icon)).setImageDrawable(userIcon);
-            mUserSwitcher.setText(mUserSwitcherController.getCurrentUserName());
+            mUserSwitcher.setText(currentUserName);
 
-            ViewGroup anchor = mView.findViewById(R.id.user_switcher_anchor);
+            KeyguardUserSwitcherAnchor anchor = mView.findViewById(R.id.user_switcher_anchor);
+
             BaseUserAdapter adapter = new BaseUserAdapter(mUserSwitcherController) {
                 @Override
                 public View getView(int position, View convertView, ViewGroup parent) {
@@ -1213,7 +1215,6 @@
 
             anchor.setOnClickListener((v) -> {
                 if (mFalsingManager.isFalseTap(LOW_PENALTY)) return;
-
                 mPopup = new KeyguardUserSwitcherPopupMenu(v.getContext(), mFalsingManager);
                 mPopup.setAnchorView(anchor);
                 mPopup.setAdapter(adapter);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
index 89d6fb5..acbea1b 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt
@@ -29,7 +29,7 @@
 
 /**
  * Translates items away/towards the hinge when the device is opened/closed. This is controlled by
- * the set of ids, which also dictact which direction to move and when, via a filter function.
+ * the set of ids, which also dictate which direction to move and when, via a filter function.
  */
 @SysUIUnfoldScope
 class KeyguardUnfoldTransition
@@ -55,7 +55,9 @@
                     ViewIdToTranslate(R.id.lockscreen_clock_view, LEFT, filterNever),
                     ViewIdToTranslate(
                         R.id.notification_stack_scroller, RIGHT, filterSplitShadeOnly),
-                    ViewIdToTranslate(R.id.wallet_button, RIGHT, filterNever)),
+                    ViewIdToTranslate(R.id.wallet_button, RIGHT, filterNever),
+                    ViewIdToTranslate(R.id.start_button, LEFT, filterNever),
+                    ViewIdToTranslate(R.id.end_button, RIGHT, filterNever)),
             progressProvider = unfoldProgressProvider)
     }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherAnchor.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherAnchor.kt
new file mode 100644
index 0000000..5f3ba72
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUserSwitcherAnchor.kt
@@ -0,0 +1,39 @@
+/*
+ * 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.keyguard
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.accessibility.AccessibilityNodeInfo
+import android.widget.LinearLayout
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import com.android.systemui.R
+
+/**
+ * Custom View for the multi-user switcher pull-down menu anchor
+ */
+class KeyguardUserSwitcherAnchor @JvmOverloads constructor(
+        context: Context,
+        attrs: AttributeSet? = null
+) : LinearLayout(context, attrs) {
+
+    override fun createAccessibilityNodeInfo(): AccessibilityNodeInfo {
+        val info = super.createAccessibilityNodeInfo()
+        AccessibilityNodeInfoCompat.wrap(info).roleDescription =
+                context.getString(R.string.accessibility_multi_user_list_switcher)
+        return info
+    }
+}
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/keyguard/LifecycleScreenStatusProvider.kt b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
index 044a57c..0a55294 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/LifecycleScreenStatusProvider.kt
@@ -41,4 +41,12 @@
     override fun onScreenTurnedOn() {
         listeners.forEach(ScreenListener::onScreenTurnedOn)
     }
+
+    override fun onScreenTurningOff() {
+        listeners.forEach(ScreenListener::onScreenTurningOff)
+    }
+
+    override fun onScreenTurningOn(ignored: Runnable) {
+        listeners.forEach(ScreenListener::onScreenTurningOn)
+    }
 }
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/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
index 77652c9..ac46c85 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
@@ -29,6 +29,7 @@
 import androidx.annotation.MainThread;
 import androidx.annotation.Nullable;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.InstanceId;
 import com.android.internal.logging.InstanceIdSequence;
 import com.android.internal.logging.UiEventLogger;
@@ -47,6 +48,7 @@
 import com.android.systemui.qs.external.TileServiceKey;
 import com.android.systemui.qs.external.TileServiceRequestController;
 import com.android.systemui.qs.logging.QSLogger;
+import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.phone.AutoTileManager;
@@ -88,6 +90,10 @@
     public static final int POSITION_AT_END = -1;
     public static final String TILES_SETTING = Secure.QS_TILES;
 
+    // Shared prefs that hold tile lifecycle info.
+    @VisibleForTesting
+    static final String TILES = "tiles_prefs";
+
     private final Context mContext;
     private final LinkedHashMap<String, QSTile> mTiles = new LinkedHashMap<>();
     protected final ArrayList<String> mTileSpecs = new ArrayList<>();
@@ -99,6 +105,7 @@
     private final InstanceIdSequence mInstanceIdSequence;
     private final CustomTileStatePersister mCustomTileStatePersister;
     private final Executor mMainExecutor;
+    private final UserFileManager mUserFileManager;
 
     private final List<Callback> mCallbacks = new ArrayList<>();
     @Nullable
@@ -135,7 +142,8 @@
             SecureSettings secureSettings,
             CustomTileStatePersister customTileStatePersister,
             TileServiceRequestController.Builder tileServiceRequestControllerBuilder,
-            TileLifecycleManager.Factory tileLifecycleManagerFactory
+            TileLifecycleManager.Factory tileLifecycleManagerFactory,
+            UserFileManager userFileManager
     ) {
         mIconController = iconController;
         mContext = context;
@@ -148,6 +156,7 @@
         mMainExecutor = mainExecutor;
         mTileServiceRequestController = tileServiceRequestControllerBuilder.create(this);
         mTileLifeCycleManagerFactory = tileLifecycleManagerFactory;
+        mUserFileManager = userFileManager;
 
         mInstanceIdSequence = new InstanceIdSequence(MAX_QS_INSTANCE_ID);
         mCentralSurfacesOptional = centralSurfacesOptional;
@@ -392,6 +401,11 @@
      */
     @Override
     public void removeTile(String spec) {
+        if (spec.startsWith(CustomTile.PREFIX)) {
+            // If the tile is removed (due to it not actually existing), mark it as removed. That
+            // way it will be marked as newly added if it appears in the future.
+            setTileAdded(CustomTile.getComponentFromSpec(spec), mCurrentUser, false);
+        }
         mMainExecutor.execute(() -> changeTileSpecs(tileSpecs-> tileSpecs.remove(spec)));
     }
 
@@ -515,7 +529,7 @@
                 lifecycleManager.onStopListening();
                 lifecycleManager.onTileRemoved();
                 mCustomTileStatePersister.removeState(new TileServiceKey(component, mCurrentUser));
-                TileLifecycleManager.setTileAdded(mContext, component, false);
+                setTileAdded(component, mCurrentUser, false);
                 lifecycleManager.flushMessagesAndUnbind();
             }
         }
@@ -552,6 +566,36 @@
         throw new RuntimeException("Default factory didn't create view for " + tile.getTileSpec());
     }
 
+    /**
+     * Check if a particular {@link CustomTile} has been added for a user and has not been removed
+     * since.
+     * @param componentName the {@link ComponentName} of the
+     *                      {@link android.service.quicksettings.TileService} associated with the
+     *                      tile.
+     * @param userId the user to check
+     */
+    public boolean isTileAdded(ComponentName componentName, int userId) {
+        return mUserFileManager
+                .getSharedPreferences(TILES, 0, userId)
+                .getBoolean(componentName.flattenToString(), false);
+    }
+
+    /**
+     * Persists whether a particular {@link CustomTile} has been added and it's currently in the
+     * set of selected tiles ({@link #mTiles}.
+     * @param componentName the {@link ComponentName} of the
+     *                      {@link android.service.quicksettings.TileService} associated
+     *                      with the tile.
+     * @param userId the user for this tile
+     * @param added {@code true} if the tile is being added, {@code false} otherwise
+     */
+    public void setTileAdded(ComponentName componentName, int userId, boolean added) {
+        mUserFileManager.getSharedPreferences(TILES, 0, userId)
+                .edit()
+                .putBoolean(componentName.flattenToString(), added)
+                .apply();
+    }
+
     protected static List<String> loadTileSpecs(Context context, String tileList) {
         final Resources res = context.getResources();
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
index a49d3fd..3e445dd 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileLifecycleManager.java
@@ -127,6 +127,10 @@
         TileLifecycleManager create(Intent intent, UserHandle userHandle);
     }
 
+    public int getUserId() {
+        return mUser.getIdentifier();
+    }
+
     public ComponentName getComponent() {
         return mIntent.getComponent();
     }
@@ -507,13 +511,4 @@
     public interface TileChangeListener {
         void onTileChanged(ComponentName tile);
     }
-
-    public static boolean isTileAdded(Context context, ComponentName component) {
-        return context.getSharedPreferences(TILES, 0).getBoolean(component.flattenToString(), false);
-    }
-
-    public static void setTileAdded(Context context, ComponentName component, boolean added) {
-        context.getSharedPreferences(TILES, 0).edit().putBoolean(component.flattenToString(),
-                added).commit();
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
index cfc57db..e86bd7a 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/external/TileServiceManager.java
@@ -109,9 +109,9 @@
     void startLifecycleManagerAndAddTile() {
         mStarted = true;
         ComponentName component = mStateManager.getComponent();
-        Context context = mServices.getContext();
-        if (!TileLifecycleManager.isTileAdded(context, component)) {
-            TileLifecycleManager.setTileAdded(context, component, true);
+        final int userId = mStateManager.getUserId();
+        if (!mServices.getHost().isTileAdded(component, userId)) {
+            mServices.getHost().setTileAdded(component, userId, true);
             mStateManager.onTileAdded();
             mStateManager.flushMessagesAndUnbind();
         }
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 438236d..30862b7 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -638,12 +638,7 @@
         // Listen for user setup
         startTracking();
 
-        screenLifecycle.addObserver(new ScreenLifecycle.Observer() {
-            @Override
-            public void onScreenTurnedOn() {
-                notifyScreenTurnedOn();
-            }
-        });
+        screenLifecycle.addObserver(mLifecycleObserver);
 
         // Connect to the service
         updateEnabledState();
@@ -951,20 +946,55 @@
         }
     }
 
-    /**
-     * Notifies the Launcher that screen turned on and ready to use
-     */
-    public void notifyScreenTurnedOn() {
-        try {
-            if (mOverviewProxy != null) {
-                mOverviewProxy.onScreenTurnedOn();
-            } else {
-                Log.e(TAG_OPS, "Failed to get overview proxy for screen turned on event.");
+    private final ScreenLifecycle.Observer mLifecycleObserver = new ScreenLifecycle.Observer() {
+        /**
+         * Notifies the Launcher that screen turned on and ready to use
+         */
+        @Override
+        public void onScreenTurnedOn() {
+            try {
+                if (mOverviewProxy != null) {
+                    mOverviewProxy.onScreenTurnedOn();
+                } else {
+                    Log.e(TAG_OPS, "Failed to get overview proxy for screen turned on event.");
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG_OPS, "Failed to call onScreenTurnedOn()", e);
             }
-        } catch (RemoteException e) {
-            Log.e(TAG_OPS, "Failed to call notifyScreenTurnedOn()", e);
         }
-    }
+
+        /**
+         * Notifies the Launcher that screen is starting to turn on.
+         */
+        @Override
+        public void onScreenTurningOff() {
+            try {
+                if (mOverviewProxy != null) {
+                    mOverviewProxy.onScreenTurningOff();
+                } else {
+                    Log.e(TAG_OPS, "Failed to get overview proxy for screen turning off event.");
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG_OPS, "Failed to call onScreenTurningOff()", e);
+            }
+        }
+
+        /**
+         * Notifies the Launcher that screen is starting to turn on.
+         */
+        @Override
+        public void onScreenTurningOn(@NonNull Runnable ignored) {
+            try {
+                if (mOverviewProxy != null) {
+                    mOverviewProxy.onScreenTurningOn();
+                } else {
+                    Log.e(TAG_OPS, "Failed to get overview proxy for screen turning on event.");
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG_OPS, "Failed to call onScreenTurningOn()", e);
+            }
+        }
+    };
 
     void notifyToggleRecentApps() {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
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/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/keyguard/KeyguardUserSwitcherAnchorTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUserSwitcherAnchorTest.kt
new file mode 100644
index 0000000..08185af
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUserSwitcherAnchorTest.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.keyguard
+
+import android.testing.AndroidTestingRunner
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class KeyguardUserSwitcherAnchorTest : SysuiTestCase() {
+
+    private lateinit var keyguardUserSwitcherAnchor: KeyguardUserSwitcherAnchor
+
+    @Before
+    fun setUp() {
+        keyguardUserSwitcherAnchor = KeyguardUserSwitcherAnchor(context)
+    }
+
+    @Test
+    fun roleDescription_is_set_to_pulldown_menu() {
+        // GIVEN
+        val roleDescriptionString =
+                context.getString(R.string.accessibility_multi_user_list_switcher)
+
+        // WHEN
+        val result = keyguardUserSwitcherAnchor.createAccessibilityNodeInfo()
+
+        // THEN
+        assertThat(
+                AccessibilityNodeInfoCompat.wrap(result).roleDescription
+        ).isEqualTo(roleDescriptionString)
+    }
+}
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/qs/QSTileHostTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
index 7dbc561..3c58b6fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSTileHostTest.java
@@ -22,6 +22,7 @@
 import static junit.framework.Assert.assertTrue;
 
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.mock;
@@ -32,11 +33,13 @@
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.SharedPreferences;
 import android.database.ContentObserver;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
 import android.testing.AndroidTestingRunner;
+import android.util.SparseArray;
 import android.view.View;
 
 import androidx.annotation.Nullable;
@@ -60,12 +63,14 @@
 import com.android.systemui.qs.external.TileServiceRequestController;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSTileImpl;
+import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.phone.AutoTileManager;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.phone.StatusBarIconController;
 import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.FakeSharedPreferences;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.FakeSettings;
 import com.android.systemui.util.settings.SecureSettings;
@@ -76,6 +81,7 @@
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -130,6 +136,10 @@
     private TileLifecycleManager.Factory mTileLifecycleManagerFactory;
     @Mock
     private TileLifecycleManager mTileLifecycleManager;
+    @Mock
+    private UserFileManager mUserFileManager;
+
+    private SparseArray<SharedPreferences> mSharedPreferencesByUser;
 
     private FakeExecutor mMainExecutor;
 
@@ -140,17 +150,29 @@
         MockitoAnnotations.initMocks(this);
         mMainExecutor = new FakeExecutor(new FakeSystemClock());
 
+        mSharedPreferencesByUser = new SparseArray<>();
+
         when(mTileServiceRequestControllerBuilder.create(any()))
                 .thenReturn(mTileServiceRequestController);
         when(mTileLifecycleManagerFactory.create(any(Intent.class), any(UserHandle.class)))
                 .thenReturn(mTileLifecycleManager);
+        when(mUserFileManager.getSharedPreferences(anyString(), anyInt(), anyInt()))
+                .thenAnswer((Answer<SharedPreferences>) invocation -> {
+                    assertEquals(QSTileHost.TILES, invocation.getArgument(0));
+                    int userId = invocation.getArgument(2);
+                    if (!mSharedPreferencesByUser.contains(userId)) {
+                        mSharedPreferencesByUser.put(userId, new FakeSharedPreferences());
+                    }
+                    return mSharedPreferencesByUser.get(userId);
+                });
 
         mSecureSettings = new FakeSettings();
         saveSetting("");
         mQSTileHost = new TestQSTileHost(mContext, mIconController, mDefaultFactory, mMainExecutor,
                 mPluginManager, mTunerService, mAutoTiles, mDumpManager, mCentralSurfaces,
                 mQSLogger, mUiEventLogger, mUserTracker, mSecureSettings, mCustomTileStatePersister,
-                mTileServiceRequestControllerBuilder, mTileLifecycleManagerFactory);
+                mTileServiceRequestControllerBuilder, mTileLifecycleManagerFactory,
+                mUserFileManager);
 
         mSecureSettings.registerContentObserverForUser(SETTING, new ContentObserver(null) {
             @Override
@@ -528,6 +550,118 @@
         assertEquals("spec1", getSetting());
     }
 
+    @Test
+    public void testIsTileAdded_true() {
+        int user = mUserTracker.getUserId();
+        getSharedPreferenecesForUser(user)
+                .edit()
+                .putBoolean(CUSTOM_TILE.flattenToString(), true)
+                .apply();
+
+        assertTrue(mQSTileHost.isTileAdded(CUSTOM_TILE, user));
+    }
+
+    @Test
+    public void testIsTileAdded_false() {
+        int user = mUserTracker.getUserId();
+        getSharedPreferenecesForUser(user)
+                .edit()
+                .putBoolean(CUSTOM_TILE.flattenToString(), false)
+                .apply();
+
+        assertFalse(mQSTileHost.isTileAdded(CUSTOM_TILE, user));
+    }
+
+    @Test
+    public void testIsTileAdded_notSet() {
+        int user = mUserTracker.getUserId();
+
+        assertFalse(mQSTileHost.isTileAdded(CUSTOM_TILE, user));
+    }
+
+    @Test
+    public void testIsTileAdded_differentUser() {
+        int user = mUserTracker.getUserId();
+        mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user)
+                .edit()
+                .putBoolean(CUSTOM_TILE.flattenToString(), true)
+                .apply();
+
+        assertFalse(mQSTileHost.isTileAdded(CUSTOM_TILE, user + 1));
+    }
+
+    @Test
+    public void testSetTileAdded_true() {
+        int user = mUserTracker.getUserId();
+        mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
+
+        assertTrue(getSharedPreferenecesForUser(user)
+                .getBoolean(CUSTOM_TILE.flattenToString(), false));
+    }
+
+    @Test
+    public void testSetTileAdded_false() {
+        int user = mUserTracker.getUserId();
+        mQSTileHost.setTileAdded(CUSTOM_TILE, user, false);
+
+        assertFalse(getSharedPreferenecesForUser(user)
+                .getBoolean(CUSTOM_TILE.flattenToString(), false));
+    }
+
+    @Test
+    public void testSetTileAdded_differentUser() {
+        int user = mUserTracker.getUserId();
+        mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
+
+        assertFalse(getSharedPreferenecesForUser(user + 1)
+                .getBoolean(CUSTOM_TILE.flattenToString(), false));
+    }
+
+    @Test
+    public void testSetTileRemoved_afterCustomTileChangedByUser() {
+        int user = mUserTracker.getUserId();
+        saveSetting(CUSTOM_TILE_SPEC);
+
+        // This will be done by TileServiceManager
+        mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
+
+        mQSTileHost.changeTilesByUser(mQSTileHost.mTileSpecs, List.of("spec1"));
+        assertFalse(getSharedPreferenecesForUser(user)
+                .getBoolean(CUSTOM_TILE.flattenToString(), false));
+    }
+
+    @Test
+    public void testSetTileRemoved_removedByUser() {
+        int user = mUserTracker.getUserId();
+        saveSetting(CUSTOM_TILE_SPEC);
+
+        // This will be done by TileServiceManager
+        mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
+
+        mQSTileHost.removeTileByUser(CUSTOM_TILE);
+        mMainExecutor.runAllReady();
+        assertFalse(getSharedPreferenecesForUser(user)
+                .getBoolean(CUSTOM_TILE.flattenToString(), false));
+    }
+
+    @Test
+    public void testSetTileRemoved_removedBySystem() {
+        int user = mUserTracker.getUserId();
+        saveSetting("spec1" + CUSTOM_TILE_SPEC);
+
+        // This will be done by TileServiceManager
+        mQSTileHost.setTileAdded(CUSTOM_TILE, user, true);
+
+        mQSTileHost.removeTile(CUSTOM_TILE_SPEC);
+        mMainExecutor.runAllReady();
+        assertFalse(getSharedPreferenecesForUser(user)
+                .getBoolean(CUSTOM_TILE.flattenToString(), false));
+    }
+
+    private SharedPreferences getSharedPreferenecesForUser(int user) {
+        return mUserFileManager.getSharedPreferences(QSTileHost.TILES, 0, user);
+    }
+
     private class TestQSTileHost extends QSTileHost {
         TestQSTileHost(Context context, StatusBarIconController iconController,
                 QSFactory defaultFactory, Executor mainExecutor,
@@ -537,11 +671,13 @@
                 UserTracker userTracker, SecureSettings secureSettings,
                 CustomTileStatePersister customTileStatePersister,
                 TileServiceRequestController.Builder tileServiceRequestControllerBuilder,
-                TileLifecycleManager.Factory tileLifecycleManagerFactory) {
+                TileLifecycleManager.Factory tileLifecycleManagerFactory,
+                UserFileManager userFileManager) {
             super(context, iconController, defaultFactory, mainExecutor, pluginManager,
                     tunerService, autoTiles, dumpManager, Optional.of(centralSurfaces), qsLogger,
                     uiEventLogger, userTracker, secureSettings, customTileStatePersister,
-                    tileServiceRequestControllerBuilder, tileLifecycleManagerFactory);
+                    tileServiceRequestControllerBuilder, tileLifecycleManagerFactory,
+                    userFileManager);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
index 573980d..8aa625a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServiceManagerTest.java
@@ -19,6 +19,14 @@
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
@@ -31,6 +39,7 @@
 import androidx.test.runner.AndroidJUnit4;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.settings.UserTracker;
 
 import org.junit.After;
@@ -38,37 +47,45 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
-import org.mockito.Mockito;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class TileServiceManagerTest extends SysuiTestCase {
 
+    @Mock
     private TileServices mTileServices;
+    @Mock
     private TileLifecycleManager mTileLifecycle;
+    @Mock
+    private UserTracker mUserTracker;
+    @Mock
+    private QSTileHost mQSTileHost;
+    @Mock
+    private Context mMockContext;
+
     private HandlerThread mThread;
     private Handler mHandler;
     private TileServiceManager mTileServiceManager;
-    private UserTracker mUserTracker;
-    private Context mMockContext;
+    private ComponentName mComponentName;
 
     @Before
     public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
         mThread = new HandlerThread("TestThread");
         mThread.start();
         mHandler = Handler.createAsync(mThread.getLooper());
-        mTileServices = Mockito.mock(TileServices.class);
-        mUserTracker = Mockito.mock(UserTracker.class);
-        Mockito.when(mUserTracker.getUserId()).thenReturn(UserHandle.USER_SYSTEM);
-        Mockito.when(mUserTracker.getUserHandle()).thenReturn(UserHandle.SYSTEM);
+        when(mUserTracker.getUserId()).thenReturn(UserHandle.USER_SYSTEM);
+        when(mUserTracker.getUserHandle()).thenReturn(UserHandle.SYSTEM);
 
-        mMockContext = Mockito.mock(Context.class);
-        Mockito.when(mTileServices.getContext()).thenReturn(mMockContext);
-        mTileLifecycle = Mockito.mock(TileLifecycleManager.class);
-        Mockito.when(mTileLifecycle.isActiveTile()).thenReturn(false);
-        ComponentName componentName = new ComponentName(mContext,
-                TileServiceManagerTest.class);
-        Mockito.when(mTileLifecycle.getComponent()).thenReturn(componentName);
+        when(mTileServices.getContext()).thenReturn(mMockContext);
+        when(mTileServices.getHost()).thenReturn(mQSTileHost);
+        when(mTileLifecycle.getUserId()).thenAnswer(invocation -> mUserTracker.getUserId());
+        when(mTileLifecycle.isActiveTile()).thenReturn(false);
+
+        mComponentName = new ComponentName(mContext, TileServiceManagerTest.class);
+        when(mTileLifecycle.getComponent()).thenReturn(mComponentName);
         mTileServiceManager = new TileServiceManager(mTileServices, mHandler, mUserTracker,
                 mTileLifecycle);
     }
@@ -80,17 +97,44 @@
     }
 
     @Test
+    public void testSetTileAddedIfNotAdded() {
+        when(mQSTileHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(false);
+        mTileServiceManager.startLifecycleManagerAndAddTile();
+
+        verify(mQSTileHost).setTileAdded(mComponentName, mUserTracker.getUserId(), true);
+    }
+
+    @Test
+    public void testNotSetTileAddedIfAdded() {
+        when(mQSTileHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(true);
+        mTileServiceManager.startLifecycleManagerAndAddTile();
+
+        verify(mQSTileHost, never()).setTileAdded(eq(mComponentName), anyInt(), eq(true));
+    }
+
+    @Test
+    public void testSetTileAddedCorrectUser() {
+        int user = 10;
+        when(mUserTracker.getUserId()).thenReturn(user);
+        when(mQSTileHost.isTileAdded(eq(mComponentName), anyInt())).thenReturn(false);
+        mTileServiceManager.startLifecycleManagerAndAddTile();
+
+        verify(mQSTileHost).setTileAdded(mComponentName, user, true);
+    }
+
+    @Test
     public void testUninstallReceiverExported() {
+        mTileServiceManager.startLifecycleManagerAndAddTile();
         ArgumentCaptor<IntentFilter> intentFilterCaptor =
                 ArgumentCaptor.forClass(IntentFilter.class);
 
-        Mockito.verify(mMockContext).registerReceiverAsUser(
-                Mockito.any(),
-                Mockito.any(),
+        verify(mMockContext).registerReceiverAsUser(
+                any(),
+                any(),
                 intentFilterCaptor.capture(),
-                Mockito.any(),
-                Mockito.any(),
-                Mockito.eq(Context.RECEIVER_EXPORTED)
+                any(),
+                any(),
+                eq(Context.RECEIVER_EXPORTED)
         );
         IntentFilter filter = intentFilterCaptor.getValue();
         assertTrue(filter.hasAction(Intent.ACTION_PACKAGE_REMOVED));
@@ -99,38 +143,41 @@
 
     @Test
     public void testSetBindRequested() {
+        mTileServiceManager.startLifecycleManagerAndAddTile();
         // Request binding.
         mTileServiceManager.setBindRequested(true);
         mTileServiceManager.setLastUpdate(0);
         mTileServiceManager.calculateBindPriority(5);
-        Mockito.verify(mTileServices, Mockito.times(2)).recalculateBindAllowance();
+        verify(mTileServices, times(2)).recalculateBindAllowance();
         assertEquals(5, mTileServiceManager.getBindPriority());
 
         // Verify same state doesn't trigger recalculating for no reason.
         mTileServiceManager.setBindRequested(true);
-        Mockito.verify(mTileServices, Mockito.times(2)).recalculateBindAllowance();
+        verify(mTileServices, times(2)).recalculateBindAllowance();
 
         mTileServiceManager.setBindRequested(false);
         mTileServiceManager.calculateBindPriority(5);
-        Mockito.verify(mTileServices, Mockito.times(3)).recalculateBindAllowance();
+        verify(mTileServices, times(3)).recalculateBindAllowance();
         assertEquals(Integer.MIN_VALUE, mTileServiceManager.getBindPriority());
     }
 
     @Test
     public void testPendingClickPriority() {
-        Mockito.when(mTileLifecycle.hasPendingClick()).thenReturn(true);
+        mTileServiceManager.startLifecycleManagerAndAddTile();
+        when(mTileLifecycle.hasPendingClick()).thenReturn(true);
         mTileServiceManager.calculateBindPriority(0);
         assertEquals(Integer.MAX_VALUE, mTileServiceManager.getBindPriority());
     }
 
     @Test
     public void testBind() {
+        mTileServiceManager.startLifecycleManagerAndAddTile();
         // Trigger binding requested and allowed.
         mTileServiceManager.setBindRequested(true);
         mTileServiceManager.setBindAllowed(true);
 
         ArgumentCaptor<Boolean> captor = ArgumentCaptor.forClass(Boolean.class);
-        Mockito.verify(mTileLifecycle, Mockito.times(1)).setBindService(captor.capture());
+        verify(mTileLifecycle, times(1)).setBindService(captor.capture());
         assertTrue((boolean) captor.getValue());
 
         mTileServiceManager.setBindRequested(false);
@@ -141,7 +188,7 @@
 
         mTileServiceManager.setBindAllowed(false);
         captor = ArgumentCaptor.forClass(Boolean.class);
-        Mockito.verify(mTileLifecycle, Mockito.times(2)).setBindService(captor.capture());
+        verify(mTileLifecycle, times(2)).setBindService(captor.capture());
         assertFalse((boolean) captor.getValue());
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
index 471ddfd..213eca8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileServicesTest.java
@@ -45,6 +45,7 @@
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.logging.QSLogger;
 import com.android.systemui.qs.tileimpl.QSFactoryImpl;
+import com.android.systemui.settings.UserFileManager;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.plugins.PluginManager;
 import com.android.systemui.statusbar.CommandQueue;
@@ -118,6 +119,8 @@
     private TileLifecycleManager.Factory mTileLifecycleManagerFactory;
     @Mock
     private TileLifecycleManager mTileLifecycleManager;
+    @Mock
+    private UserFileManager mUserFileManager;
 
     @Before
     public void setUp() throws Exception {
@@ -149,7 +152,8 @@
                 mSecureSettings,
                 mock(CustomTileStatePersister.class),
                 mTileServiceRequestControllerBuilder,
-                mTileLifecycleManagerFactory);
+                mTileLifecycleManagerFactory,
+                mUserFileManager);
         mTileService = new TestTileServices(host, provider, mBroadcastDispatcher,
                 mUserTracker, mKeyguardStateController, mCommandQueue);
     }
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/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
index 87fca1f..7e07040 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/updates/DeviceFoldStateProviderTest.kt
@@ -30,13 +30,13 @@
 import com.android.systemui.unfold.updates.screen.ScreenStatusProvider.ScreenListener
 import com.android.systemui.util.mockito.any
 import com.google.common.truth.Truth.assertThat
+import java.util.concurrent.Executor
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.MockitoAnnotations
-import java.util.concurrent.Executor
 import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
 @SmallTest
@@ -331,6 +331,47 @@
         assertThat(foldUpdates).containsExactly(FOLD_UPDATE_START_CLOSING)
     }
 
+    @Test
+    fun screenOff_whileFolded_hingeAngleProviderRemainsOff() {
+        setFoldState(folded = true)
+        assertThat(testHingeAngleProvider.isStarted).isFalse()
+
+        screenOnStatusProvider.notifyScreenTurningOff()
+
+        assertThat(testHingeAngleProvider.isStarted).isFalse()
+    }
+
+    @Test
+    fun screenOff_whileUnfolded_hingeAngleProviderStops() {
+        setFoldState(folded = false)
+        assertThat(testHingeAngleProvider.isStarted).isTrue()
+
+        screenOnStatusProvider.notifyScreenTurningOff()
+
+        assertThat(testHingeAngleProvider.isStarted).isFalse()
+    }
+
+    @Test
+    fun screenOn_whileUnfoldedAndScreenOff_hingeAngleProviderStarted() {
+        setFoldState(folded = false)
+        screenOnStatusProvider.notifyScreenTurningOff()
+        assertThat(testHingeAngleProvider.isStarted).isFalse()
+
+        screenOnStatusProvider.notifyScreenTurningOn()
+
+        assertThat(testHingeAngleProvider.isStarted).isTrue()
+    }
+
+    @Test
+    fun screenOn_whileFolded_hingeAngleRemainsOff() {
+        setFoldState(folded = true)
+        assertThat(testHingeAngleProvider.isStarted).isFalse()
+
+        screenOnStatusProvider.notifyScreenTurningOn()
+
+        assertThat(testHingeAngleProvider.isStarted).isFalse()
+    }
+
     private fun setupForegroundActivityType(isHomeActivity: Boolean?) {
         whenever(activityTypeProvider.isHomeActivity).thenReturn(isHomeActivity)
     }
@@ -391,6 +432,14 @@
         fun notifyScreenTurnedOn() {
             callbacks.forEach { it.onScreenTurnedOn() }
         }
+
+        fun notifyScreenTurningOn() {
+            callbacks.forEach { it.onScreenTurningOn() }
+        }
+
+        fun notifyScreenTurningOff() {
+            callbacks.forEach { it.onScreenTurningOff() }
+        }
     }
 
     private class TestHingeAngleProvider : HingeAngleProvider {
@@ -398,11 +447,11 @@
         var isStarted: Boolean = false
 
         override fun start() {
-            isStarted = true;
+            isStarted = true
         }
 
         override fun stop() {
-            isStarted = false;
+            isStarted = false
         }
 
         override fun addCallback(listener: Consumer<Float>) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/FakeSharedPreferencesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/FakeSharedPreferencesTest.kt
new file mode 100644
index 0000000..d886ffd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/FakeSharedPreferencesTest.kt
@@ -0,0 +1,259 @@
+/*
+ * 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.util
+
+import android.content.SharedPreferences
+import androidx.test.filters.SmallTest
+import androidx.test.runner.AndroidJUnit4
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.mockito.eq
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.anyString
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.verifyNoMoreInteractions
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class FakeSharedPreferencesTest : SysuiTestCase() {
+
+    @Mock
+    private lateinit var listener: SharedPreferences.OnSharedPreferenceChangeListener
+
+    private lateinit var sharedPreferences: SharedPreferences
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        sharedPreferences = FakeSharedPreferences()
+    }
+
+    @Test
+    fun testGetString_default() {
+        val default = "default"
+        val result = sharedPreferences.getString("key", default)
+        assertThat(result).isEqualTo(default)
+    }
+
+    @Test
+    fun testGetStringSet_default() {
+        val default = setOf("one", "two")
+        val result = sharedPreferences.getStringSet("key", default)
+        assertThat(result).isEqualTo(default)
+    }
+
+    @Test
+    fun testGetInt_default() {
+        val default = 10
+        val result = sharedPreferences.getInt("key", default)
+        assertThat(result).isEqualTo(default)
+    }
+
+    @Test
+    fun testGetLong_default() {
+        val default = 11L
+        val result = sharedPreferences.getLong("key", default)
+        assertThat(result).isEqualTo(default)
+    }
+
+    @Test
+    fun testGetFloat_default() {
+        val default = 1.3f
+        val result = sharedPreferences.getFloat("key", default)
+        assertThat(result).isEqualTo(default)
+    }
+
+    @Test
+    fun testGetBoolean_default() {
+        val default = true
+        val result = sharedPreferences.getBoolean("key", default)
+        assertThat(result).isEqualTo(default)
+    }
+
+    @Test
+    fun testPutValuesAndRetrieve() {
+        val editor = sharedPreferences.edit()
+        val data = listOf<Data<*>>(
+            Data(
+                "keyString",
+                "value",
+                SharedPreferences.Editor::putString,
+                { getString(it, "") }
+            ),
+            Data(
+                "keyStringSet",
+                setOf("one", "two"),
+                SharedPreferences.Editor::putStringSet,
+                { getStringSet(it, emptySet()) }
+            ),
+            Data("keyInt", 10, SharedPreferences.Editor::putInt, { getInt(it, 0) }),
+            Data("keyLong", 11L, SharedPreferences.Editor::putLong, { getLong(it, 0L) }),
+            Data(
+                "keyFloat",
+                1.3f,
+                SharedPreferences.Editor::putFloat,
+                { getFloat(it, 0f) }
+            ),
+            Data(
+                "keyBoolean",
+                true,
+                SharedPreferences.Editor::putBoolean,
+                { getBoolean(it, false) }
+            )
+        )
+
+        data.fold(editor) { ed, d ->
+            d.set(ed)
+        }
+        editor.commit()
+
+        data.forEach {
+            assertThat(it.get(sharedPreferences)).isEqualTo(it.value)
+        }
+    }
+
+    @Test
+    fun testContains() {
+        sharedPreferences.edit().putInt("key", 10).commit()
+
+        assertThat(sharedPreferences.contains("key")).isTrue()
+        assertThat(sharedPreferences.contains("other")).isFalse()
+    }
+
+    @Test
+    fun testOverwrite() {
+        sharedPreferences.edit().putInt("key", 10).commit()
+        sharedPreferences.edit().putInt("key", 11).commit()
+
+        assertThat(sharedPreferences.getInt("key", 0)).isEqualTo(11)
+    }
+
+    @Test
+    fun testDeleteString() {
+        sharedPreferences.edit().putString("key", "value").commit()
+        sharedPreferences.edit().putString("key", null).commit()
+
+        assertThat(sharedPreferences.contains("key")).isFalse()
+    }
+
+    @Test
+    fun testDeleteAndReplaceString() {
+        sharedPreferences.edit().putString("key", "value").commit()
+        sharedPreferences.edit().putString("key", "other").putString("key", null).commit()
+
+        assertThat(sharedPreferences.getString("key", "")).isEqualTo("other")
+    }
+
+    @Test
+    fun testDeleteStringSet() {
+        sharedPreferences.edit().putStringSet("key", setOf("one")).commit()
+        sharedPreferences.edit().putStringSet("key", setOf("two")).commit()
+
+        assertThat(sharedPreferences.getStringSet("key", emptySet())).isEqualTo(setOf("two"))
+    }
+
+    @Test
+    fun testClear() {
+        sharedPreferences.edit().putInt("keyInt", 1).putString("keyString", "a").commit()
+        sharedPreferences.edit().clear().commit()
+
+        assertThat(sharedPreferences.contains("keyInt")).isFalse()
+        assertThat(sharedPreferences.contains("keyString")).isFalse()
+    }
+
+    @Test
+    fun testClearAndWrite() {
+        sharedPreferences.edit().putInt("key", 10).commit()
+        sharedPreferences.edit().putInt("key", 11).clear().commit()
+
+        assertThat(sharedPreferences.getInt("key", 0)).isEqualTo(11)
+    }
+
+    @Test
+    fun testListenerNotifiedOnChanges() {
+        sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
+
+        sharedPreferences.edit().putInt("keyInt", 10).putString("keyString", "value").commit()
+
+        verify(listener).onSharedPreferenceChanged(sharedPreferences, "keyInt")
+        verify(listener).onSharedPreferenceChanged(sharedPreferences, "keyString")
+        verifyNoMoreInteractions(listener)
+    }
+
+    @Test
+    fun testListenerNotifiedOnClear() {
+        sharedPreferences.edit().putInt("keyInt", 10).commit()
+        sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
+
+        sharedPreferences.edit().clear().commit()
+
+        verify(listener).onSharedPreferenceChanged(sharedPreferences, null)
+        verifyNoMoreInteractions(listener)
+    }
+
+    @Test
+    fun testListenerNotifiedOnRemoval() {
+        sharedPreferences.edit()
+            .putString("keyString", "a")
+            .putStringSet("keySet", setOf("a"))
+            .commit()
+
+        sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
+        sharedPreferences.edit().putString("keyString", null).putStringSet("keySet", null).commit()
+
+        verify(listener).onSharedPreferenceChanged(sharedPreferences, "keyString")
+        verify(listener).onSharedPreferenceChanged(sharedPreferences, "keySet")
+        verifyNoMoreInteractions(listener)
+    }
+
+    @Test
+    fun testListenerUnregistered() {
+        sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
+        sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener)
+        sharedPreferences.edit().putInt("key", 10).commit()
+
+        verify(listener, never()).onSharedPreferenceChanged(eq(sharedPreferences), anyString())
+    }
+
+    @Test
+    fun testSharedPreferencesOnlyModifiedOnCommit() {
+        sharedPreferences.edit().putInt("key", 10)
+
+        assertThat(sharedPreferences.contains("key")).isFalse()
+    }
+
+    private data class Data<T>(
+        val key: String,
+        val value: T,
+        private val setter: SharedPreferences.Editor.(String, T) -> SharedPreferences.Editor,
+        private val getter: SharedPreferences.(String) -> T
+    ) {
+        fun set(editor: SharedPreferences.Editor): SharedPreferences.Editor {
+            return editor.setter(key, value)
+        }
+
+        fun get(sharedPreferences: SharedPreferences): T {
+            return sharedPreferences.getter(key)
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt
new file mode 100644
index 0000000..4a881a7
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/FakeSharedPreferences.kt
@@ -0,0 +1,164 @@
+/*
+ * 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.util
+
+import android.content.SharedPreferences
+
+/**
+ * Fake [SharedPreferences] to use within tests
+ *
+ * This will act in the same way as a real one for a particular file, but will store all the
+ * data in memory in the instance.
+ *
+ * [SharedPreferences.Editor.apply] and [SharedPreferences.Editor.commit] both act in the same way,
+ * synchronously modifying the stored data. Listeners are dispatched in the same thread, also
+ * synchronously.
+ */
+class FakeSharedPreferences : SharedPreferences {
+    private val data = mutableMapOf<String, Any>()
+    private val listeners = mutableSetOf<SharedPreferences.OnSharedPreferenceChangeListener>()
+
+    override fun getAll(): Map<String, *> {
+        return data
+    }
+
+    override fun getString(key: String, defValue: String?): String? {
+        return data.getOrDefault(key, defValue) as? String?
+    }
+
+    override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
+        return data.getOrDefault(key, defValues) as? MutableSet<String>?
+    }
+
+    override fun getInt(key: String, defValue: Int): Int {
+        return data.getOrDefault(key, defValue) as Int
+    }
+
+    override fun getLong(key: String, defValue: Long): Long {
+        return data.getOrDefault(key, defValue) as Long
+    }
+
+    override fun getFloat(key: String, defValue: Float): Float {
+        return data.getOrDefault(key, defValue) as Float
+    }
+
+    override fun getBoolean(key: String, defValue: Boolean): Boolean {
+        return data.getOrDefault(key, defValue) as Boolean
+    }
+
+    override fun contains(key: String): Boolean {
+        return key in data
+    }
+
+    override fun edit(): SharedPreferences.Editor {
+        return Editor()
+    }
+
+    override fun registerOnSharedPreferenceChangeListener(
+        listener: SharedPreferences.OnSharedPreferenceChangeListener
+    ) {
+        listeners.add(listener)
+    }
+
+    override fun unregisterOnSharedPreferenceChangeListener(
+        listener: SharedPreferences.OnSharedPreferenceChangeListener
+    ) {
+        listeners.remove(listener)
+    }
+
+    private inner class Editor : SharedPreferences.Editor {
+
+        private var clear = false
+        private val changes = mutableMapOf<String, Any>()
+        private val removals = mutableSetOf<String>()
+
+        override fun putString(key: String, value: String?): SharedPreferences.Editor {
+            if (value != null) {
+                changes[key] = value
+            } else {
+                removals.add(key)
+            }
+            return this
+        }
+
+        override fun putStringSet(
+            key: String,
+            values: MutableSet<String>?
+        ): SharedPreferences.Editor {
+            if (values != null) {
+                changes[key] = values
+            } else {
+                removals.add(key)
+            }
+            return this
+        }
+
+        override fun putInt(key: String, value: Int): SharedPreferences.Editor {
+            changes[key] = value
+            return this
+        }
+
+        override fun putLong(key: String, value: Long): SharedPreferences.Editor {
+            changes[key] = value
+            return this
+        }
+
+        override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
+            changes[key] = value
+            return this
+        }
+
+        override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
+            changes[key] = value
+            return this
+        }
+
+        override fun remove(key: String): SharedPreferences.Editor {
+            removals.add(key)
+            return this
+        }
+
+        override fun clear(): SharedPreferences.Editor {
+            clear = true
+            return this
+        }
+
+        override fun commit(): Boolean {
+            if (clear) {
+                data.clear()
+            }
+            removals.forEach { data.remove(it) }
+            data.putAll(changes)
+            val keys = removals + data.keys
+            if (clear || removals.isNotEmpty() || data.isNotEmpty()) {
+                listeners.forEach { listener ->
+                    if (clear) {
+                        listener.onSharedPreferenceChanged(this@FakeSharedPreferences, null)
+                    }
+                    keys.forEach {
+                        listener.onSharedPreferenceChanged(this@FakeSharedPreferences, it)
+                    }
+                }
+            }
+            return true
+        }
+
+        override fun apply() {
+            commit()
+        }
+    }
+}
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
index e8038fd..19cfc80 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/DeviceFoldStateProvider.kt
@@ -65,6 +65,7 @@
     private val halfOpenedTimeoutMillis: Int = config.halfFoldedTimeoutMillis
 
     private var isFolded = false
+    private var isScreenOn = false
     private var isUnfoldHandled = true
 
     override fun start() {
@@ -198,6 +199,25 @@
                 isUnfoldHandled = true
             }
         }
+
+        override fun onScreenTurningOn() {
+            isScreenOn = true
+            updateHingeAngleProviderState()
+        }
+
+        override fun onScreenTurningOff() {
+            isScreenOn = false
+            updateHingeAngleProviderState()
+        }
+    }
+
+    /** While the screen is off or the device is folded, hinge angle updates are not needed. */
+    private fun updateHingeAngleProviderState() {
+        if (isScreenOn && !isFolded) {
+            hingeAngleProvider.start()
+        } else {
+            hingeAngleProvider.stop()
+        }
     }
 
     private inner class HingeAngleListener : Consumer<Float> {
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
index 3fc5d61..577137c 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/hinge/HingeSensorAngleProvider.kt
@@ -30,8 +30,10 @@
 
     private val sensorListener = HingeAngleSensorListener()
     private val listeners: MutableList<Consumer<Float>> = arrayListOf()
+    var started = false
 
     override fun start() = executor.execute {
+        if (started) return@execute
         Trace.beginSection("HingeSensorAngleProvider#start")
         val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
         sensorManager.registerListener(
@@ -40,10 +42,13 @@
             SensorManager.SENSOR_DELAY_FASTEST
         )
         Trace.endSection()
+        started = true
     }
 
     override fun stop() = executor.execute {
+        if (!started) return@execute
         sensorManager.unregisterListener(sensorListener)
+        started = false
     }
 
     override fun removeCallback(listener: Consumer<Float>) {
diff --git a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt
index d95e050..f09b53d 100644
--- a/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt
+++ b/packages/SystemUI/unfold/src/com/android/systemui/unfold/updates/screen/ScreenStatusProvider.kt
@@ -25,5 +25,15 @@
          * Called when the screen is on and ready (windows are drawn and screen blocker is removed)
          */
         fun onScreenTurnedOn()
+
+        /**
+         * Called when the screen is starting to be turned off.
+         */
+        fun onScreenTurningOff()
+
+        /**
+         * Called when the screen is starting to be turned on.
+         */
+        fun onScreenTurningOn()
     }
 }
diff --git a/services/Android.bp b/services/Android.bp
index 70692a6..1e4ce19 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -102,6 +102,7 @@
         ":services.profcollect-sources",
         ":services.restrictions-sources",
         ":services.searchui-sources",
+        ":services.selectiontoolbar-sources",
         ":services.smartspace-sources",
         ":services.speech-sources",
         ":services.systemcaptions-sources",
@@ -157,6 +158,7 @@
         "services.profcollect",
         "services.restrictions",
         "services.searchui",
+        "services.selectiontoolbar",
         "services.smartspace",
         "services.speech",
         "services.systemcaptions",
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index b5aa7b1..4f3fd64 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -33,6 +33,7 @@
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.display.BrightnessSynchronizer;
+import com.android.server.display.config.AutoBrightness;
 import com.android.server.display.config.BrightnessThresholds;
 import com.android.server.display.config.BrightnessThrottlingMap;
 import com.android.server.display.config.BrightnessThrottlingPoint;
@@ -65,13 +66,13 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
+import java.util.Locale;
 
 import javax.xml.datatype.DatatypeConfigurationException;
 
 /**
- *  Reads and stores display-specific configurations.
- *  File format:
- *  <pre>
+ * Reads and stores display-specific configurations. File format:
+ * <pre>
  *  {@code
  *    <displayConfiguration>
  *      <densityMapping>
@@ -147,6 +148,15 @@
  *       <quirk>canSetBrightnessViaHwc</quirk>
  *      </quirks>
  *
+ *      <autoBrightness>
+ *           <brighteningLightDebounceMillis>
+ *              2000
+ *           </brighteningLightDebounceMillis>
+ *          <darkeningLightDebounceMillis>
+ *              1000
+ *          </darkeningLightDebounceMillis>
+ *      </autoBrightness>
+ *
  *      <screenBrightnessRampFastDecrease>0.01</screenBrightnessRampFastDecrease>
  *      <screenBrightnessRampFastIncrease>0.02</screenBrightnessRampFastIncrease>
  *      <screenBrightnessRampSlowDecrease>0.03</screenBrightnessRampSlowDecrease>
@@ -224,6 +234,9 @@
     // Length of the ambient light horizon used to calculate short-term estimate of ambient light.
     private static final int AMBIENT_LIGHT_SHORT_HORIZON_MILLIS = 2000;
 
+    // Invalid value of AutoBrightness brightening and darkening light debounce
+    private static final int INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE = -1;
+
     @VisibleForTesting
     static final float HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT = 0.5f;
 
@@ -281,6 +294,14 @@
     private String mLoadedFrom = null;
     private Spline mSdrToHdrRatioSpline;
 
+    // Represents the auto-brightness brightening light debounce.
+    private long mAutoBrightnessBrighteningLightDebounce =
+            INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE;
+
+    // Represents the auto-brightness darkening light debounce.
+    private long mAutoBrightnessDarkeningLightDebounce =
+            INVALID_AUTO_BRIGHTNESS_LIGHT_DEBOUNCE;
+
     // Brightness Throttling data may be updated via the DeviceConfig. Here we store the original
     // data, which comes from the ddc, and the current one, which may be the DeviceConfig
     // overwritten value.
@@ -293,8 +314,8 @@
     }
 
     /**
-     * Creates an instance for the specified display.
-     * Tries to find a file with identifier in the following priority order:
+     * Creates an instance for the specified display. Tries to find a file with identifier in the
+     * following priority order:
      * <ol>
      *     <li>physicalDisplayId</li>
      *     <li>physicalDisplayId without a stable flag (old system)</li>
@@ -314,11 +335,12 @@
     }
 
     /**
-     * Creates an instance using global values since no display device config xml exists.
-     * Uses values from config or PowerManager.
+     * Creates an instance using global values since no display device config xml exists. Uses
+     * values from config or PowerManager.
      *
-     * @param context
-     * @param useConfigXml
+     * @param context      The context from which the DisplayDeviceConfig is to be constructed.
+     * @param useConfigXml A flag indicating if values are to be loaded from the configuration file,
+     *                     or the default values.
      * @return A configuration instance.
      */
     public static DisplayDeviceConfig create(Context context, boolean useConfigXml) {
@@ -450,8 +472,8 @@
     }
 
     /**
-     * Calculates the backlight value, as recognised by the HAL, from the brightness value
-     * given that the rest of the system deals with.
+     * Calculates the backlight value, as recognised by the HAL, from the brightness value given
+     * that the rest of the system deals with.
      *
      * @param brightness value on the framework scale of 0-1
      * @return backlight value on the HAL scale of 0-1
@@ -502,13 +524,13 @@
 
         if (DEBUG) {
             Slog.d(TAG, "getHdrBrightnessFromSdr: sdr brightness " + brightness
-                + " backlight " + backlight
-                + " nits " + nits
-                + " ratio " + ratio
-                + " hdrNits " + hdrNits
-                + " hdrBacklight " + hdrBacklight
-                + " hdrBrightness " + hdrBrightness
-                );
+                    + " backlight " + backlight
+                    + " nits " + nits
+                    + " ratio " + ratio
+                    + " hdrNits " + hdrNits
+                    + " hdrBacklight " + hdrBacklight
+                    + " hdrBrightness " + hdrBrightness
+            );
         }
         return hdrBrightness;
     }
@@ -590,8 +612,8 @@
 
     /**
      * @param quirkValue The quirk to test.
-     * @return {@code true} if the specified quirk is present in this configuration,
-     * {@code false} otherwise.
+     * @return {@code true} if the specified quirk is present in this configuration, {@code false}
+     * otherwise.
      */
     public boolean hasQuirk(String quirkValue) {
         return mQuirks != null && mQuirks.contains(quirkValue);
@@ -625,6 +647,20 @@
         return BrightnessThrottlingData.create(mBrightnessThrottlingData);
     }
 
+    /**
+     * @return Auto brightness darkening light debounce
+     */
+    public long getAutoBrightnessDarkeningLightDebounce() {
+        return mAutoBrightnessDarkeningLightDebounce;
+    }
+
+    /**
+     * @return Auto brightness brightening light debounce
+     */
+    public long getAutoBrightnessBrighteningLightDebounce() {
+        return mAutoBrightnessBrighteningLightDebounce;
+    }
+
     @Override
     public String toString() {
         return "DisplayDeviceConfig{"
@@ -663,14 +699,18 @@
                 + ", mProximitySensor=" + mProximitySensor
                 + ", mRefreshRateLimitations= " + Arrays.toString(mRefreshRateLimitations.toArray())
                 + ", mDensityMapping= " + mDensityMapping
+                + ", mAutoBrightnessBrighteningLightDebounce= "
+                + mAutoBrightnessBrighteningLightDebounce
+                + ", mAutoBrightnessDarkeningLightDebounce= "
+                + mAutoBrightnessDarkeningLightDebounce
                 + "}";
     }
 
     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);
@@ -719,6 +759,7 @@
                 loadProxSensorFromDdc(config);
                 loadAmbientHorizonFromDdc(config);
                 loadBrightnessChangeThresholds(config);
+                loadAutoBrightnessConfigValues(config);
             } else {
                 Slog.w(TAG, "DisplayDeviceConfig file is null");
             }
@@ -899,8 +940,8 @@
             if (i > 0) {
                 if (nits[i] < nits[i - 1]) {
                     Slog.e(TAG, "sdrHdrRatioMap must be non-decreasing, ignoring rest "
-                                + " of configuration. nits: " + nits[i] + " < "
-                                + nits[i - 1]);
+                            + " of configuration. nits: " + nits[i] + " < "
+                            + nits[i - 1]);
                     return null;
                 }
             }
@@ -927,7 +968,7 @@
         final List<BrightnessThrottlingPoint> points = map.getBrightnessThrottlingPoint();
         // At least 1 point is guaranteed by the display device config schema
         List<BrightnessThrottlingData.ThrottlingLevel> throttlingLevels =
-            new ArrayList<>(points.size());
+                new ArrayList<>(points.size());
 
         boolean badConfig = false;
         for (BrightnessThrottlingPoint point : points) {
@@ -938,7 +979,7 @@
             }
 
             throttlingLevels.add(new BrightnessThrottlingData.ThrottlingLevel(
-                convertThermalStatus(status), point.getBrightness().floatValue()));
+                    convertThermalStatus(status), point.getBrightness().floatValue()));
         }
 
         if (!badConfig) {
@@ -947,6 +988,41 @@
         }
     }
 
+    private void loadAutoBrightnessConfigValues(DisplayConfiguration config) {
+        loadAutoBrightnessBrighteningLightDebounce(config.getAutoBrightness());
+        loadAutoBrightnessDarkeningLightDebounce(config.getAutoBrightness());
+    }
+
+    /**
+     * Loads the auto-brightness brightening light debounce. Internally, this takes care of loading
+     * the value from the display config, and if not present, falls back to config.xml.
+     */
+    private void loadAutoBrightnessBrighteningLightDebounce(AutoBrightness autoBrightnessConfig) {
+        if (autoBrightnessConfig == null
+                || autoBrightnessConfig.getBrighteningLightDebounceMillis() == null) {
+            mAutoBrightnessBrighteningLightDebounce = mContext.getResources().getInteger(
+                    com.android.internal.R.integer.config_autoBrightnessBrighteningLightDebounce);
+        } else {
+            mAutoBrightnessBrighteningLightDebounce =
+                    autoBrightnessConfig.getBrighteningLightDebounceMillis().intValue();
+        }
+    }
+
+    /**
+     * Loads the auto-brightness darkening light debounce. Internally, this takes care of loading
+     * the value from the display config, and if not present, falls back to config.xml.
+     */
+    private void loadAutoBrightnessDarkeningLightDebounce(AutoBrightness autoBrightnessConfig) {
+        if (autoBrightnessConfig == null
+                || autoBrightnessConfig.getDarkeningLightDebounceMillis() == null) {
+            mAutoBrightnessDarkeningLightDebounce = mContext.getResources().getInteger(
+                    com.android.internal.R.integer.config_autoBrightnessDarkeningLightDebounce);
+        } else {
+            mAutoBrightnessDarkeningLightDebounce =
+                    autoBrightnessConfig.getDarkeningLightDebounceMillis().intValue();
+        }
+    }
+
     private void loadBrightnessMapFromConfigXml() {
         // Use the config.xml mapping
         final Resources res = mContext.getResources();
@@ -1058,17 +1134,17 @@
                     PowerManager.BRIGHTNESS_MIN, PowerManager.BRIGHTNESS_MAX, mBacklight[i]);
         }
         mBrightnessToBacklightSpline = mInterpolationType == INTERPOLATION_LINEAR
-            ? Spline.createLinearSpline(mBrightness, mBacklight)
-            : Spline.createSpline(mBrightness, mBacklight);
+                ? Spline.createLinearSpline(mBrightness, mBacklight)
+                : Spline.createSpline(mBrightness, mBacklight);
         mBacklightToBrightnessSpline = mInterpolationType == INTERPOLATION_LINEAR
-            ? Spline.createLinearSpline(mBacklight, mBrightness)
-            : Spline.createSpline(mBacklight, mBrightness);
+                ? Spline.createLinearSpline(mBacklight, mBrightness)
+                : Spline.createSpline(mBacklight, mBrightness);
         mBacklightToNitsSpline = mInterpolationType == INTERPOLATION_LINEAR
-            ? Spline.createLinearSpline(mBacklight, mNits)
-            : Spline.createSpline(mBacklight, mNits);
+                ? Spline.createLinearSpline(mBacklight, mNits)
+                : Spline.createSpline(mBacklight, mNits);
         mNitsToBacklightSpline = mInterpolationType == INTERPOLATION_LINEAR
-            ? Spline.createLinearSpline(mNits, mBacklight)
-            : Spline.createSpline(mNits, mBacklight);
+                ? Spline.createLinearSpline(mNits, mBacklight)
+                : Spline.createSpline(mNits, mBacklight);
     }
 
     private void loadQuirks(DisplayConfiguration config) {
@@ -1111,7 +1187,7 @@
                 if (mHbmData.minimumHdrPercentOfScreen > 1
                         || mHbmData.minimumHdrPercentOfScreen < 0) {
                     Slog.w(TAG, "Invalid minimum HDR percent of screen: "
-                                    + String.valueOf(mHbmData.minimumHdrPercentOfScreen));
+                            + String.valueOf(mHbmData.minimumHdrPercentOfScreen));
                     mHbmData.minimumHdrPercentOfScreen = HDR_PERCENT_OF_SCREEN_REQUIRED_DEFAULT;
                 }
             } else {
@@ -1235,7 +1311,7 @@
                     ambientBrightnessThresholds.getDarkeningThresholds();
 
             final BigDecimal ambientBrighteningThreshold = brighteningAmbientLux.getMinimum();
-            final BigDecimal ambientDarkeningThreshold =  darkeningAmbientLux.getMinimum();
+            final BigDecimal ambientDarkeningThreshold = darkeningAmbientLux.getMinimum();
 
             if (ambientBrighteningThreshold != null) {
                 mAmbientLuxBrighteningMinThreshold = ambientBrighteningThreshold.floatValue();
@@ -1330,8 +1406,8 @@
         }
 
         /**
-         * @return True if the sensor matches both the specified name and type, or one if only
-         * one is specified (not-empty). Always returns false if both parameters are null or empty.
+         * @return True if the sensor matches both the specified name and type, or one if only one
+         * is specified (not-empty). Always returns false if both parameters are null or empty.
          */
         public boolean matches(String sensorName, String sensorType) {
             final boolean isNameSpecified = !TextUtils.isEmpty(sensorName);
@@ -1446,6 +1522,7 @@
                 return otherThrottlingLevel.thermalStatus == this.thermalStatus
                         && otherThrottlingLevel.brightness == this.brightness;
             }
+
             @Override
             public int hashCode() {
                 int result = 1;
@@ -1455,8 +1532,11 @@
             }
         }
 
-        static public BrightnessThrottlingData create(List<ThrottlingLevel> throttlingLevels)
-        {
+
+        /**
+         * Creates multiple teperature based throttling levels of brightness
+         */
+        public static BrightnessThrottlingData create(List<ThrottlingLevel> throttlingLevels) {
             if (throttlingLevels == null || throttlingLevels.size() == 0) {
                 Slog.e(TAG, "BrightnessThrottlingData received null or empty throttling levels");
                 return null;
@@ -1498,8 +1578,9 @@
         }
 
         static public BrightnessThrottlingData create(BrightnessThrottlingData other) {
-            if (other == null)
+            if (other == null) {
                 return null;
+            }
 
             return BrightnessThrottlingData.create(other.throttlingLevels);
         }
@@ -1508,8 +1589,8 @@
         @Override
         public String toString() {
             return "BrightnessThrottlingData{"
-                + "throttlingLevels:" + throttlingLevels
-                + "} ";
+                    + "throttlingLevels:" + throttlingLevels
+                    + "} ";
         }
 
         @Override
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 8781a8d..9c86076 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -985,10 +985,10 @@
                     screenBrighteningThresholds, screenDarkeningThresholds, screenThresholdLevels,
                     screenDarkeningMinThreshold, screenBrighteningMinThreshold);
 
-            long brighteningLightDebounce = resources.getInteger(
-                    com.android.internal.R.integer.config_autoBrightnessBrighteningLightDebounce);
-            long darkeningLightDebounce = resources.getInteger(
-                    com.android.internal.R.integer.config_autoBrightnessDarkeningLightDebounce);
+            long brighteningLightDebounce = mDisplayDeviceConfig
+                    .getAutoBrightnessBrighteningLightDebounce();
+            long darkeningLightDebounce = mDisplayDeviceConfig
+                    .getAutoBrightnessDarkeningLightDebounce();
             boolean autoBrightnessResetAmbientLuxAfterWarmUp = resources.getBoolean(
                     com.android.internal.R.bool.config_autoBrightnessResetAmbientLuxAfterWarmUp);
 
@@ -1204,6 +1204,7 @@
         }
         assert(state != Display.STATE_UNKNOWN);
 
+        boolean skipRampBecauseOfProximityChangeToNegative = false;
         // Apply the proximity sensor.
         if (mProximitySensor != null) {
             if (mPowerRequest.useProximitySensor && state != Display.STATE_OFF) {
@@ -1241,6 +1242,7 @@
                 // the screen back on.  Also turn it back on if we've been asked to ignore the
                 // prox sensor temporarily.
                 mScreenOffBecauseOfProximity = false;
+                skipRampBecauseOfProximityChangeToNegative = true;
                 sendOnProximityNegativeWithWakelock();
             }
         } else {
@@ -1313,9 +1315,6 @@
         }
 
         final boolean autoBrightnessAdjustmentChanged = updateAutoBrightnessAdjustment();
-        if (autoBrightnessAdjustmentChanged) {
-            mTemporaryAutoBrightnessAdjustment = Float.NaN;
-        }
 
         // Use the autobrightness adjustment override if set.
         final float autoBrightnessAdjustment;
@@ -1523,8 +1522,8 @@
 
             final boolean wasOrWillBeInVr =
                     (state == Display.STATE_VR || oldState == Display.STATE_VR);
-            final boolean initialRampSkip =
-                    state == Display.STATE_ON && mSkipRampState != RAMP_STATE_SKIP_NONE;
+            final boolean initialRampSkip = (state == Display.STATE_ON && mSkipRampState
+                    != RAMP_STATE_SKIP_NONE) || skipRampBecauseOfProximityChangeToNegative;
             // While dozing, sometimes the brightness is split into buckets. Rather than animating
             // through the buckets, which is unlikely to be smooth in the first place, just jump
             // right to the suggested brightness.
@@ -2291,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();
@@ -2367,6 +2367,7 @@
         }
         mAutoBrightnessAdjustment = mPendingAutoBrightnessAdjustment;
         mPendingAutoBrightnessAdjustment = Float.NaN;
+        mTemporaryAutoBrightnessAdjustment = Float.NaN;
         return true;
     }
 
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index 25d187f..0769406 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -1725,16 +1725,6 @@
                             win.mAttrs.insetsFlags.appearance & APPEARANCE_LIGHT_STATUS_BARS,
                             new Rect(win.getFrame())));
                     mStatusBarColorCheckedBounds.union(sTmpRect);
-                    // Check if current activity is letterboxed in order create a LetterboxDetails
-                    // component to be passed to SysUI for status bar treatment
-                    final ActivityRecord currentActivity = win.getActivityRecord();
-                    if (currentActivity != null) {
-                        final LetterboxDetails currentLetterboxDetails = currentActivity
-                                .mLetterboxUiController.getLetterboxDetails();
-                        if (currentLetterboxDetails != null) {
-                            mLetterboxDetails.add(currentLetterboxDetails);
-                        }
-                    }
                 }
             }
 
@@ -1752,6 +1742,17 @@
                     mNavBarBackgroundWindow = win;
                 }
             }
+
+            // Check if current activity is letterboxed in order create a LetterboxDetails
+            // component to be passed to SysUI for status bar treatment
+            final ActivityRecord currentActivity = win.getActivityRecord();
+            if (currentActivity != null) {
+                final LetterboxDetails currentLetterboxDetails = currentActivity
+                        .mLetterboxUiController.getLetterboxDetails();
+                if (currentLetterboxDetails != null) {
+                    mLetterboxDetails.add(currentLetterboxDetails);
+                }
+            }
         } else if (win.isDimming()) {
             if (mStatusBar != null) {
                 addStatusBarAppearanceRegionsForDimmingWindow(
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/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index 09044e7..073b131c 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -45,6 +45,8 @@
                 <xs:element type="highBrightnessMode" name="highBrightnessMode" minOccurs="0"
                             maxOccurs="1"/>
                 <xs:element type="displayQuirks" name="quirks" minOccurs="0" maxOccurs="1" />
+                <xs:element type="autoBrightness" name="autoBrightness" minOccurs="0"
+                            maxOccurs="1" />
                 <xs:element type="nonNegativeDecimal" name="screenBrightnessRampFastDecrease">
                     <xs:annotation name="final"/>
                 </xs:element>
@@ -101,6 +103,21 @@
 
     <!-- Type definitions -->
 
+    <xs:complexType name="autoBrightness">
+        <xs:sequence>
+            <!-- Sets the debounce for autoBrightness brightening in millis-->
+            <xs:element name="brighteningLightDebounceMillis" type="xs:nonNegativeInteger"
+                        minOccurs="0" maxOccurs="1">
+                <xs:annotation name="final"/>
+            </xs:element>
+            <!-- Sets the debounce for autoBrightness darkening in millis-->
+            <xs:element name="darkeningLightDebounceMillis" type="xs:nonNegativeInteger"
+                        minOccurs="0" maxOccurs="1">
+                <xs:annotation name="final"/>
+            </xs:element>
+        </xs:sequence>
+    </xs:complexType>
+
     <xs:complexType name="displayQuirks">
         <xs:sequence>
             <xs:element name="quirk" type="xs:string" minOccurs="0" maxOccurs="unbounded" />
@@ -341,5 +358,4 @@
             <xs:annotation name="final"/>
         </xs:element>
     </xs:complexType>
-
 </xs:schema>
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index e8b13ca..e9a9269 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -1,6 +1,14 @@
 // Signature format: 2.0
 package com.android.server.display.config {
 
+  public class AutoBrightness {
+    ctor public AutoBrightness();
+    method public final java.math.BigInteger getBrighteningLightDebounceMillis();
+    method public final java.math.BigInteger getDarkeningLightDebounceMillis();
+    method public final void setBrighteningLightDebounceMillis(java.math.BigInteger);
+    method public final void setDarkeningLightDebounceMillis(java.math.BigInteger);
+  }
+
   public class BrightnessThresholds {
     ctor public BrightnessThresholds();
     method @NonNull public final java.math.BigDecimal getMinimum();
@@ -40,6 +48,7 @@
     method @NonNull public final com.android.server.display.config.Thresholds getAmbientBrightnessChangeThresholds();
     method public final java.math.BigInteger getAmbientLightHorizonLong();
     method public final java.math.BigInteger getAmbientLightHorizonShort();
+    method public com.android.server.display.config.AutoBrightness getAutoBrightness();
     method @Nullable public final com.android.server.display.config.DensityMapping getDensityMapping();
     method @NonNull public final com.android.server.display.config.Thresholds getDisplayBrightnessChangeThresholds();
     method public com.android.server.display.config.HighBrightnessMode getHighBrightnessMode();
@@ -58,6 +67,7 @@
     method public final void setAmbientBrightnessChangeThresholds(@NonNull com.android.server.display.config.Thresholds);
     method public final void setAmbientLightHorizonLong(java.math.BigInteger);
     method public final void setAmbientLightHorizonShort(java.math.BigInteger);
+    method public void setAutoBrightness(com.android.server.display.config.AutoBrightness);
     method public final void setDensityMapping(@Nullable com.android.server.display.config.DensityMapping);
     method public final void setDisplayBrightnessChangeThresholds(@NonNull com.android.server.display.config.Thresholds);
     method public void setHighBrightnessMode(com.android.server.display.config.HighBrightnessMode);
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 66c9f55..8fd4b5a 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -338,6 +338,8 @@
             "com.android.server.contentcapture.ContentCaptureManagerService";
     private static final String TRANSLATION_MANAGER_SERVICE_CLASS =
             "com.android.server.translation.TranslationManagerService";
+    private static final String SELECTION_TOOLBAR_MANAGER_SERVICE_CLASS =
+            "com.android.server.selectiontoolbar.SelectionToolbarManagerService";
     private static final String MUSIC_RECOGNITION_MANAGER_SERVICE_CLASS =
             "com.android.server.musicrecognition.MusicRecognitionManagerService";
     private static final String SYSTEM_CAPTIONS_MANAGER_SERVICE_CLASS =
@@ -2634,6 +2636,11 @@
             Slog.d(TAG, "TranslationService not defined by OEM");
         }
 
+        // Selection toolbar service
+        t.traceBegin("StartSelectionToolbarManagerService");
+        mSystemServiceManager.startService(SELECTION_TOOLBAR_MANAGER_SERVICE_CLASS);
+        t.traceEnd();
+
         // NOTE: ClipboardService depends on ContentCapture and Autofill
         t.traceBegin("StartClipboardService");
         mSystemServiceManager.startService(ClipboardService.class);
diff --git a/services/selectiontoolbar/Android.bp b/services/selectiontoolbar/Android.bp
new file mode 100644
index 0000000..cc6405f
--- /dev/null
+++ b/services/selectiontoolbar/Android.bp
@@ -0,0 +1,22 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+filegroup {
+    name: "services.selectiontoolbar-sources",
+    srcs: ["java/**/*.java"],
+    path: "java",
+    visibility: ["//frameworks/base/services"],
+}
+
+java_library_static {
+    name: "services.selectiontoolbar",
+    defaults: ["platform_service_defaults"],
+    srcs: [":services.selectiontoolbar-sources"],
+    libs: ["services.core"],
+}
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 7939303..03ea613 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -86,6 +86,8 @@
                 0.0f);
         assertEquals(mDisplayDeviceConfig.getScreenBrighteningMinThreshold(), 0.001, 0.000001f);
         assertEquals(mDisplayDeviceConfig.getScreenDarkeningMinThreshold(), 0.002, 0.000001f);
+        assertEquals(mDisplayDeviceConfig.getAutoBrightnessBrighteningLightDebounce(), 2000);
+        assertEquals(mDisplayDeviceConfig.getAutoBrightnessDarkeningLightDebounce(), 1000);
 
         // Todo(brup): Add asserts for BrightnessThrottlingData, DensityMapping,
         // HighBrightnessModeData AmbientLightSensor, RefreshRateLimitations and ProximitySensor.
@@ -109,6 +111,10 @@
                 +           "<nits>800.0</nits>\n"
                 +       "</point>\n"
                 +   "</screenBrightnessMap>\n"
+                +   "<autoBrightness>\n"
+                +       "<brighteningLightDebounceMillis>2000</brighteningLightDebounceMillis>\n"
+                +       "<darkeningLightDebounceMillis>1000</darkeningLightDebounceMillis>\n"
+                +   "</autoBrightness>\n"
                 +   "<highBrightnessMode enabled=\"true\">\n"
                 +       "<transitionPoint>0.62</transitionPoint>\n"
                 +       "<minimumLux>10000</minimumLux>\n"
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index 21839aa..fde6e3c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -102,7 +102,6 @@
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
-import org.mockito.Mockito;
 
 /**
  * Tests for Size Compatibility mode.
@@ -2165,6 +2164,40 @@
     }
 
     @Test
+    public void testLetterboxDetailsForStatusBar_letterboxNotOverlappingStatusBar() {
+        final DisplayContent display = new TestDisplayContent.Builder(mAtm, 1000, 2800)
+                .setNotch(100)
+                .build();
+        setUpApp(display);
+        TestWindowState statusBar = addStatusBar(mActivity.mDisplayContent);
+        spyOn(statusBar);
+        doReturn(new Rect(0, 0, statusBar.mRequestedWidth, statusBar.mRequestedHeight))
+                .when(statusBar).getFrame();
+        addWindowToActivity(mActivity); // Add a window to the activity so that we can get an
+        // appearance inside letterboxDetails
+        // Prepare unresizable activity with max aspect ratio
+        prepareUnresizable(mActivity, /* maxAspect */ 1.1f, SCREEN_ORIENTATION_UNSPECIFIED);
+        // Refresh the letterbox
+        mActivity.mRootWindowContainer.performSurfacePlacement();
+
+        Rect mBounds = new Rect(mActivity.getWindowConfiguration().getBounds());
+        assertEquals(mBounds, new Rect(0, 750, 1000, 1950));
+
+        DisplayPolicy displayPolicy = mActivity.getDisplayContent().getDisplayPolicy();
+        LetterboxDetails[] expectedLetterboxDetails = {new LetterboxDetails(
+                mBounds,
+                mActivity.getDisplayContent().getBounds(),
+                mActivity.findMainWindow().mAttrs.insetsFlags.appearance
+        )};
+
+        // Check that letterboxDetails actually gets passed to SysUI
+        StatusBarManagerInternal statusBarManager = displayPolicy.getStatusBarManagerInternal();
+        verify(statusBarManager).onSystemBarAttributesChanged(anyInt(), anyInt(),
+                any(), anyBoolean(), anyInt(),
+                any(InsetsVisibilities.class), isNull(), eq(expectedLetterboxDetails));
+    }
+
+    @Test
     public void testSplitScreenLetterboxDetailsForStatusBar_twoLetterboxedApps() {
         mAtm.mDevEnableNonResizableMultiWindow = true;
         setUpDisplaySizeWithApp(2800, 1000);
@@ -2785,7 +2818,7 @@
         return w;
     }
 
-    private static void addStatusBar(DisplayContent displayContent) {
+    private static TestWindowState addStatusBar(DisplayContent displayContent) {
         final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
         doReturn(true).when(displayPolicy).hasStatusBar();
         displayPolicy.onConfigurationChanged();
@@ -2806,6 +2839,7 @@
 
         displayPolicy.addWindowLw(statusBar, attrs);
         displayPolicy.layoutWindowLw(statusBar, null, displayContent.mDisplayFrames);
+        return statusBar;
     }
 
     /**
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();