Merge "Fix leakage of pointer location" into tm-qpr-dev
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 90c37d1..4ea0c32 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -308,7 +308,7 @@
  * <li>The <b>foreground lifetime</b> of an activity happens between a call to
  * {@link android.app.Activity#onResume} until a corresponding call to
  * {@link android.app.Activity#onPause}.  During this time the activity is
- * in visible, active and interacting with the user.  An activity
+ * visible, active and interacting with the user.  An activity
  * can frequently go between the resumed and paused states -- for example when
  * the device goes to sleep, when an activity result is delivered, when a new
  * intent is delivered -- so the code in these methods should be fairly
diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java
index 4fc3254..61cca00 100644
--- a/core/java/android/app/ActivityOptions.java
+++ b/core/java/android/app/ActivityOptions.java
@@ -1441,6 +1441,11 @@
     }
 
     /** @hide */
+    public void setRemoteTransition(@Nullable RemoteTransition remoteTransition) {
+        mRemoteTransition = remoteTransition;
+    }
+
+    /** @hide */
     public static ActivityOptions fromBundle(Bundle bOptions) {
         return bOptions != null ? new ActivityOptions(bOptions) : null;
     }
diff --git a/core/java/android/content/integrity/AppInstallMetadata.java b/core/java/android/content/integrity/AppInstallMetadata.java
index 9874890..91b0007 100644
--- a/core/java/android/content/integrity/AppInstallMetadata.java
+++ b/core/java/android/content/integrity/AppInstallMetadata.java
@@ -129,9 +129,18 @@
     @Override
     public String toString() {
         return String.format(
-                "AppInstallMetadata { PackageName = %s, AppCerts = %s, InstallerName = %s,"
-                    + " InstallerCerts = %s, VersionCode = %d, PreInstalled = %b, StampPresent ="
-                    + " %b, StampVerified = %b, StampTrusted = %b, StampCert = %s }",
+                "AppInstallMetadata {"
+                    + " PackageName = %s,"
+                    + " AppCerts = %s,"
+                    + " AppCertsLineage = %s,"
+                    + " InstallerName = %s,"
+                    + " InstallerCerts = %s,"
+                    + " VersionCode = %d,"
+                    + " PreInstalled = %b,"
+                    + " StampPresent = %b,"
+                    + " StampVerified = %b,"
+                    + " StampTrusted = %b,"
+                    + " StampCert = %s }",
                 mPackageName,
                 mAppCertificates,
                 mAppCertificateLineage,
diff --git a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
index 336ef7a..41822e7 100644
--- a/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
+++ b/core/java/android/hardware/camera2/impl/CameraExtensionSessionImpl.java
@@ -1693,8 +1693,10 @@
                         ((i != idx) || notifyCurrentIndex)) {
                     TotalCaptureResult result = previewMap.valueAt(i).second;
                     Long timestamp = result.get(CaptureResult.SENSOR_TIMESTAMP);
-                    mCaptureResultHandler.onCaptureCompleted(timestamp,
-                            initializeFilteredResults(result));
+                    if (mCaptureResultHandler != null) {
+                        mCaptureResultHandler.onCaptureCompleted(timestamp,
+                                initializeFilteredResults(result));
+                    }
 
                     Log.w(TAG, "Preview frame drop with timestamp: " + previewMap.keyAt(i));
                     final long ident = Binder.clearCallingIdentity();
diff --git a/core/java/android/view/RemoteAnimationTarget.java b/core/java/android/view/RemoteAnimationTarget.java
index a468951..44f419a 100644
--- a/core/java/android/view/RemoteAnimationTarget.java
+++ b/core/java/android/view/RemoteAnimationTarget.java
@@ -45,6 +45,7 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.proto.ProtoOutputStream;
+import android.window.TaskSnapshot;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
@@ -234,6 +235,12 @@
      */
     public @ColorInt int backgroundColor;
 
+    /**
+     * Whether the activity is going to show IME on the target window after the app transition.
+     * @see TaskSnapshot#hasImeSurface() that used the task snapshot during animating task.
+     */
+    public boolean willShowImeOnTarget;
+
     public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent,
             Rect clipRect, Rect contentInsets, int prefixOrderIndex, Point position,
             Rect localBounds, Rect screenSpaceBounds,
@@ -294,12 +301,21 @@
         hasAnimatingParent = in.readBoolean();
         backgroundColor = in.readInt();
         showBackdrop = in.readBoolean();
+        willShowImeOnTarget = in.readBoolean();
     }
 
     public void setShowBackdrop(boolean shouldShowBackdrop) {
         showBackdrop = shouldShowBackdrop;
     }
 
+    public void setWillShowImeOnTarget(boolean showImeOnTarget) {
+        willShowImeOnTarget = showImeOnTarget;
+    }
+
+    public boolean willShowImeOnTarget() {
+        return willShowImeOnTarget;
+    }
+
     @Override
     public int describeContents() {
         return 0;
@@ -328,6 +344,7 @@
         dest.writeBoolean(hasAnimatingParent);
         dest.writeInt(backgroundColor);
         dest.writeBoolean(showBackdrop);
+        dest.writeBoolean(willShowImeOnTarget);
     }
 
     public void dump(PrintWriter pw, String prefix) {
@@ -350,6 +367,7 @@
         pw.print(prefix); pw.print("hasAnimatingParent="); pw.print(hasAnimatingParent);
         pw.print(prefix); pw.print("backgroundColor="); pw.print(backgroundColor);
         pw.print(prefix); pw.print("showBackdrop="); pw.print(showBackdrop);
+        pw.print(prefix); pw.print("willShowImeOnTarget="); pw.print(willShowImeOnTarget);
     }
 
     public void dumpDebug(ProtoOutputStream proto, long fieldId) {
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 427aa0f..7690af6 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -10879,8 +10879,8 @@
 
     private void registerCompatOnBackInvokedCallback() {
         mCompatOnBackInvokedCallback = () -> {
-                sendBackKeyEvent(KeyEvent.ACTION_DOWN);
-                sendBackKeyEvent(KeyEvent.ACTION_UP);
+            sendBackKeyEvent(KeyEvent.ACTION_DOWN);
+            sendBackKeyEvent(KeyEvent.ACTION_UP);
         };
         mOnBackInvokedDispatcher.registerOnBackInvokedCallback(
                 OnBackInvokedDispatcher.PRIORITY_DEFAULT, mCompatOnBackInvokedCallback);
diff --git a/core/java/android/view/contentcapture/MainContentCaptureSession.java b/core/java/android/view/contentcapture/MainContentCaptureSession.java
index 98ef4e7..90384b5 100644
--- a/core/java/android/view/contentcapture/MainContentCaptureSession.java
+++ b/core/java/android/view/contentcapture/MainContentCaptureSession.java
@@ -64,6 +64,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.NoSuchElementException;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
@@ -630,7 +631,11 @@
         mComponentName = null;
         mEvents = null;
         if (mDirectServiceInterface != null) {
-            mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
+            try {
+                mDirectServiceInterface.asBinder().unlinkToDeath(mDirectServiceVulture, 0);
+            } catch (NoSuchElementException e) {
+                Log.w(TAG, "IContentCaptureDirectManager does not exist");
+            }
         }
         mDirectServiceInterface = null;
         mHandler.removeMessages(MSG_FLUSH);
diff --git a/core/java/android/window/ProxyOnBackInvokedDispatcher.java b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
index 8ad1093..49acde9 100644
--- a/core/java/android/window/ProxyOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
@@ -122,7 +122,7 @@
         }
         for (Pair<OnBackInvokedCallback, Integer> callbackPair : mCallbacks) {
             int priority = callbackPair.second;
-            if (priority >= 0) {
+            if (priority >= PRIORITY_DEFAULT) {
                 mActualDispatcher.registerOnBackInvokedCallback(priority, callbackPair.first);
             } else {
                 mActualDispatcher.registerSystemOnBackInvokedCallback(callbackPair.first);
diff --git a/core/java/android/window/TransitionInfo.java b/core/java/android/window/TransitionInfo.java
index 1f3841a..b263b08 100644
--- a/core/java/android/window/TransitionInfo.java
+++ b/core/java/android/window/TransitionInfo.java
@@ -116,6 +116,9 @@
     /** The first unused bit. This can be used by remotes to attach custom flags to this change. */
     public static final int FLAG_FIRST_CUSTOM = 1 << 10;
 
+    /** The container is going to show IME on its task after the transition. */
+    public static final int FLAG_WILL_IME_SHOWN = 1 << 11;
+
     /** @hide */
     @IntDef(prefix = { "FLAG_" }, value = {
             FLAG_NONE,
@@ -129,7 +132,8 @@
             FLAG_DISPLAY_HAS_ALERT_WINDOWS,
             FLAG_IS_INPUT_METHOD,
             FLAG_IS_EMBEDDED,
-            FLAG_FIRST_CUSTOM
+            FLAG_FIRST_CUSTOM,
+            FLAG_WILL_IME_SHOWN
     })
     public @interface ChangeFlags {}
 
diff --git a/core/java/com/android/internal/app/BlockedAppStreamingActivity.java b/core/java/com/android/internal/app/BlockedAppStreamingActivity.java
index ca111a4..7c24892 100644
--- a/core/java/com/android/internal/app/BlockedAppStreamingActivity.java
+++ b/core/java/com/android/internal/app/BlockedAppStreamingActivity.java
@@ -36,6 +36,7 @@
     private static final String EXTRA_BLOCKED_ACTIVITY_INFO =
             PACKAGE_NAME + ".extra.BLOCKED_ACTIVITY_INFO";
     private static final String EXTRA_STREAMED_DEVICE = PACKAGE_NAME + ".extra.STREAMED_DEVICE";
+    private static final String BLOCKED_COMPONENT_PLAYSTORE = "com.android.vending";
     private static final String BLOCKED_COMPONENT_SETTINGS = "com.android.settings";
 
     @Override
@@ -62,21 +63,25 @@
                 mAlertParams.mTitle =
                         getString(R.string.app_streaming_blocked_title_for_permission_dialog);
                 mAlertParams.mMessage =
-                        getString(R.string.app_streaming_blocked_message_for_permission_dialog,
-                                streamedDeviceName);
+                        getString(R.string.app_streaming_blocked_message, streamedDeviceName);
+            } else if (TextUtils.equals(activityInfo.packageName, BLOCKED_COMPONENT_PLAYSTORE)) {
+                mAlertParams.mTitle =
+                        getString(R.string.app_streaming_blocked_title_for_playstore_dialog);
+                mAlertParams.mMessage =
+                        getString(R.string.app_streaming_blocked_message, streamedDeviceName);
             } else if (TextUtils.equals(activityInfo.packageName, BLOCKED_COMPONENT_SETTINGS)) {
                 mAlertParams.mTitle =
                         getString(R.string.app_streaming_blocked_title_for_settings_dialog);
                 mAlertParams.mMessage =
-                        getString(R.string.app_streaming_blocked_message, streamedDeviceName);
+                        getString(R.string.app_streaming_blocked_message_for_settings_dialog,
+                                streamedDeviceName);
             } else {
-                mAlertParams.mTitle =
-                        getString(R.string.app_streaming_blocked_title, appLabel);
+                // No title required
                 mAlertParams.mMessage =
                         getString(R.string.app_streaming_blocked_message, streamedDeviceName);
             }
         } else {
-            mAlertParams.mTitle = getString(R.string.app_blocked_title);
+            // No title required
             mAlertParams.mMessage = getString(R.string.app_blocked_message, appLabel);
         }
         mAlertParams.mPositiveButtonText = getString(android.R.string.ok);
diff --git a/core/java/com/android/internal/util/OWNERS b/core/java/com/android/internal/util/OWNERS
index 354dd9a..1808bd5 100644
--- a/core/java/com/android/internal/util/OWNERS
+++ b/core/java/com/android/internal/util/OWNERS
@@ -5,3 +5,4 @@
 per-file Protocol* = etancohen@google.com, lorenzo@google.com
 per-file State* = jchalard@google.com, lorenzo@google.com, satk@google.com
 per-file *Dump* = file:/core/java/com/android/internal/util/dump/OWNERS
+per-file *Screenshot* = file:/packages/SystemUI/src/com/android/systemui/screenshot/OWNERS
diff --git a/core/java/com/android/internal/util/ScreenshotHelper.java b/core/java/com/android/internal/util/ScreenshotHelper.java
index f4f438b..8af2450 100644
--- a/core/java/com/android/internal/util/ScreenshotHelper.java
+++ b/core/java/com/android/internal/util/ScreenshotHelper.java
@@ -1,7 +1,6 @@
 package com.android.internal.util;
 
 import static android.content.Intent.ACTION_USER_SWITCHED;
-import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_OTHER;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -29,6 +28,10 @@
 import android.os.UserHandle;
 import android.util.Log;
 import android.view.WindowManager;
+import android.view.WindowManager.ScreenshotSource;
+import android.view.WindowManager.ScreenshotType;
+
+import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.Objects;
 import java.util.function.Consumer;
@@ -42,24 +45,28 @@
      * Describes a screenshot request (to make it easier to pass data through to the handler).
      */
     public static class ScreenshotRequest implements Parcelable {
-        private int mSource;
-        private boolean mHasStatusBar;
-        private boolean mHasNavBar;
-        private Bundle mBitmapBundle;
-        private Rect mBoundsInScreen;
-        private Insets mInsets;
-        private int mTaskId;
-        private int mUserId;
-        private ComponentName mTopComponent;
+        private final int mSource;
+        private final Bundle mBitmapBundle;
+        private final Rect mBoundsInScreen;
+        private final Insets mInsets;
+        private final int mTaskId;
+        private final int mUserId;
+        private final ComponentName mTopComponent;
 
-        ScreenshotRequest(int source, boolean hasStatus, boolean hasNav) {
+        @VisibleForTesting
+        public ScreenshotRequest(int source) {
             mSource = source;
-            mHasStatusBar = hasStatus;
-            mHasNavBar = hasNav;
+            mBitmapBundle = null;
+            mBoundsInScreen = null;
+            mInsets = null;
+            mTaskId = -1;
+            mUserId = -1;
+            mTopComponent = null;
         }
 
-        ScreenshotRequest(int source, Bundle bitmapBundle, Rect boundsInScreen, Insets insets,
-                int taskId, int userId, ComponentName topComponent) {
+        @VisibleForTesting
+        public ScreenshotRequest(int source, Bundle bitmapBundle, Rect boundsInScreen,
+                Insets insets, int taskId, int userId, ComponentName topComponent) {
             mSource = source;
             mBitmapBundle = bitmapBundle;
             mBoundsInScreen = boundsInScreen;
@@ -71,16 +78,21 @@
 
         ScreenshotRequest(Parcel in) {
             mSource = in.readInt();
-            mHasStatusBar = in.readBoolean();
-            mHasNavBar = in.readBoolean();
-
             if (in.readInt() == 1) {
                 mBitmapBundle = in.readBundle(getClass().getClassLoader());
-                mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader(), android.graphics.Rect.class);
-                mInsets = in.readParcelable(Insets.class.getClassLoader(), android.graphics.Insets.class);
+                mBoundsInScreen = in.readParcelable(Rect.class.getClassLoader(), Rect.class);
+                mInsets = in.readParcelable(Insets.class.getClassLoader(), Insets.class);
                 mTaskId = in.readInt();
                 mUserId = in.readInt();
-                mTopComponent = in.readParcelable(ComponentName.class.getClassLoader(), android.content.ComponentName.class);
+                mTopComponent = in.readParcelable(ComponentName.class.getClassLoader(),
+                        ComponentName.class);
+            } else {
+                mBitmapBundle = null;
+                mBoundsInScreen = null;
+                mInsets = null;
+                mTaskId = -1;
+                mUserId = -1;
+                mTopComponent = null;
             }
         }
 
@@ -88,14 +100,6 @@
             return mSource;
         }
 
-        public boolean getHasStatusBar() {
-            return mHasStatusBar;
-        }
-
-        public boolean getHasNavBar() {
-            return mHasNavBar;
-        }
-
         public Bundle getBitmapBundle() {
             return mBitmapBundle;
         }
@@ -112,7 +116,6 @@
             return mTaskId;
         }
 
-
         public int getUserId() {
             return mUserId;
         }
@@ -129,8 +132,6 @@
         @Override
         public void writeToParcel(Parcel dest, int flags) {
             dest.writeInt(mSource);
-            dest.writeBoolean(mHasStatusBar);
-            dest.writeBoolean(mHasNavBar);
             if (mBitmapBundle == null) {
                 dest.writeInt(0);
             } else {
@@ -144,7 +145,8 @@
             }
         }
 
-        public static final @NonNull Parcelable.Creator<ScreenshotRequest> CREATOR =
+        @NonNull
+        public static final Parcelable.Creator<ScreenshotRequest> CREATOR =
                 new Parcelable.Creator<ScreenshotRequest>() {
 
                     @Override
@@ -254,113 +256,71 @@
 
     /**
      * Request a screenshot be taken.
-     *
+     * <p>
      * Added to support reducing unit test duration; the method variant without a timeout argument
      * is recommended for general use.
      *
-     * @param screenshotType     The type of screenshot, for example either
-     *                           {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN}
-     *                           or
-     *                           {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
-     * @param hasStatus          {@code true} if the status bar is currently showing. {@code false}
-     *                           if not.
-     * @param hasNav             {@code true} if the navigation bar is currently showing. {@code
-     *                           false} if not.
-     * @param source             The source of the screenshot request. One of
-     *                           {SCREENSHOT_GLOBAL_ACTIONS, SCREENSHOT_KEY_CHORD,
-     *                           SCREENSHOT_OVERVIEW, SCREENSHOT_OTHER}
-     * @param handler            A handler used in case the screenshot times out
-     * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
-     *                           screenshot was taken.
+     * @param screenshotType The type of screenshot, defined by {@link ScreenshotType}
+     * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
+     * @param handler used to process messages received from the screenshot service
+     * @param completionConsumer receives the URI of the captured screenshot, once saved or
+     *         null if no screenshot was saved
      */
-    public void takeScreenshot(final int screenshotType, final boolean hasStatus,
-            final boolean hasNav, int source, @NonNull Handler handler,
-            @Nullable Consumer<Uri> completionConsumer) {
-        ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, hasStatus, hasNav);
-        takeScreenshot(screenshotType, SCREENSHOT_TIMEOUT_MS, handler, screenshotRequest,
+    public void takeScreenshot(@ScreenshotType int screenshotType, @ScreenshotSource int source,
+            @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) {
+        ScreenshotRequest screenshotRequest = new ScreenshotRequest(source);
+        takeScreenshot(screenshotType, handler, screenshotRequest, SCREENSHOT_TIMEOUT_MS,
                 completionConsumer);
     }
 
     /**
-     * Request a screenshot be taken, with provided reason.
-     *
-     * @param screenshotType     The type of screenshot, for example either
-     *                           {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN}
-     *                           or
-     *                           {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
-     * @param hasStatus          {@code true} if the status bar is currently showing. {@code false}
-     *                           if
-     *                           not.
-     * @param hasNav             {@code true} if the navigation bar is currently showing. {@code
-     *                           false}
-     *                           if not.
-     * @param handler            A handler used in case the screenshot times out
-     * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
-     *                           screenshot was taken.
-     */
-    public void takeScreenshot(final int screenshotType, final boolean hasStatus,
-            final boolean hasNav, @NonNull Handler handler,
-            @Nullable Consumer<Uri> completionConsumer) {
-        takeScreenshot(screenshotType, hasStatus, hasNav, SCREENSHOT_TIMEOUT_MS, handler,
-                completionConsumer);
-    }
-
-    /**
-     * Request a screenshot be taken with a specific timeout.
-     *
+     * Request a screenshot be taken.
+     * <p>
      * Added to support reducing unit test duration; the method variant without a timeout argument
      * is recommended for general use.
      *
-     * @param screenshotType     The type of screenshot, for example either
-     *                           {@link android.view.WindowManager#TAKE_SCREENSHOT_FULLSCREEN}
-     *                           or
-     *                           {@link android.view.WindowManager#TAKE_SCREENSHOT_SELECTED_REGION}
-     * @param hasStatus          {@code true} if the status bar is currently showing. {@code false}
-     *                           if
-     *                           not.
-     * @param hasNav             {@code true} if the navigation bar is currently showing. {@code
-     *                           false}
-     *                           if not.
-     * @param timeoutMs          If the screenshot hasn't been completed within this time period,
-     *                           the screenshot attempt will be cancelled and `completionConsumer`
-     *                           will be run.
-     * @param handler            A handler used in case the screenshot times out
-     * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
-     *                           screenshot was taken.
+     * @param screenshotType The type of screenshot, defined by {@link ScreenshotType}
+     * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
+     * @param handler used to process messages received from the screenshot service
+     * @param timeoutMs time limit for processing, intended only for testing
+     * @param completionConsumer receives the URI of the captured screenshot, once saved or
+     *         null if no screenshot was saved
      */
-    public void takeScreenshot(final int screenshotType, final boolean hasStatus,
-            final boolean hasNav, long timeoutMs, @NonNull Handler handler,
-            @Nullable Consumer<Uri> completionConsumer) {
-        ScreenshotRequest screenshotRequest = new ScreenshotRequest(SCREENSHOT_OTHER, hasStatus,
-                hasNav);
-        takeScreenshot(screenshotType, timeoutMs, handler, screenshotRequest, completionConsumer);
+    @VisibleForTesting
+    public void takeScreenshot(@ScreenshotType int screenshotType, @ScreenshotSource int source,
+            @NonNull Handler handler, long timeoutMs, @Nullable Consumer<Uri> completionConsumer) {
+        ScreenshotRequest screenshotRequest = new ScreenshotRequest(source);
+        takeScreenshot(screenshotType, handler, screenshotRequest, timeoutMs, completionConsumer);
     }
 
     /**
      * Request that provided image be handled as if it was a screenshot.
      *
-     * @param screenshotBundle   Bundle containing the buffer and color space of the screenshot.
-     * @param boundsInScreen     The bounds in screen coordinates that the bitmap orginated from.
-     * @param insets             The insets that the image was shown with, inside the screenbounds.
-     * @param taskId             The taskId of the task that the screen shot was taken of.
-     * @param userId             The userId of user running the task provided in taskId.
-     * @param topComponent       The component name of the top component running in the task.
-     * @param handler            A handler used in case the screenshot times out
-     * @param completionConsumer Consumes `false` if a screenshot was not taken, and `true` if the
-     *                           screenshot was taken.
+     * @param screenshotBundle Bundle containing the buffer and color space of the screenshot.
+     * @param boundsInScreen The bounds in screen coordinates that the bitmap originated from.
+     * @param insets The insets that the image was shown with, inside the screen bounds.
+     * @param taskId The taskId of the task that the screen shot was taken of.
+     * @param userId The userId of user running the task provided in taskId.
+     * @param topComponent The component name of the top component running in the task.
+     * @param source The source of the screenshot request, defined by {@link ScreenshotSource}
+     * @param handler A handler used in case the screenshot times out
+     * @param completionConsumer receives the URI of the captured screenshot, once saved or
+     *         null if no screenshot was saved
      */
     public void provideScreenshot(@NonNull Bundle screenshotBundle, @NonNull Rect boundsInScreen,
-            @NonNull Insets insets, int taskId, int userId, ComponentName topComponent, int source,
-            @NonNull Handler handler, @Nullable Consumer<Uri> completionConsumer) {
-        ScreenshotRequest screenshotRequest =
-                new ScreenshotRequest(source, screenshotBundle, boundsInScreen, insets, taskId,
-                        userId, topComponent);
-        takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, SCREENSHOT_TIMEOUT_MS,
-                handler, screenshotRequest, completionConsumer);
+            @NonNull Insets insets, int taskId, int userId, ComponentName topComponent,
+            @ScreenshotSource int source, @NonNull Handler handler,
+            @Nullable Consumer<Uri> completionConsumer) {
+        ScreenshotRequest screenshotRequest = new ScreenshotRequest(source, screenshotBundle,
+                boundsInScreen, insets, taskId, userId, topComponent);
+        takeScreenshot(WindowManager.TAKE_SCREENSHOT_PROVIDED_IMAGE, handler, screenshotRequest,
+                SCREENSHOT_TIMEOUT_MS,
+                completionConsumer);
     }
 
-    private void takeScreenshot(final int screenshotType, long timeoutMs, @NonNull Handler handler,
-            ScreenshotRequest screenshotRequest, @Nullable Consumer<Uri> completionConsumer) {
+    private void takeScreenshot(@ScreenshotType int screenshotType, @NonNull Handler handler,
+            ScreenshotRequest screenshotRequest, long timeoutMs,
+            @Nullable Consumer<Uri> completionConsumer) {
         synchronized (mScreenshotLock) {
 
             final Runnable mScreenshotTimeout = () -> {
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index d528385..a3b05b5 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5479,6 +5479,8 @@
     <string name="app_streaming_blocked_title_for_fingerprint_dialog">Continue on phone</string>
     <!-- Title of the dialog shown when the microphone permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
     <string name="app_streaming_blocked_title_for_microphone_dialog">Microphone unavailable</string>
+    <!-- Title of the dialog shown when the play store is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
+    <string name="app_streaming_blocked_title_for_playstore_dialog">Play Store unavailable</string>
     <!-- Title of the dialog shown when the settings is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
     <string name="app_streaming_blocked_title_for_settings_dialog" product="tv">Android TV settings unavailable</string>
     <!-- Title of the dialog shown when the settings is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
@@ -5486,23 +5488,23 @@
     <!-- Title of the dialog shown when the settings is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
     <string name="app_streaming_blocked_title_for_settings_dialog" product="default">Phone settings unavailable</string>
     <!-- Message shown when an app is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
-    <string name="app_streaming_blocked_message" product="tv">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g>. Try on your Android TV device instead.</string>
+    <string name="app_streaming_blocked_message" product="tv">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your Android TV device instead.</string>
     <!-- Message shown when an app is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
-    <string name="app_streaming_blocked_message" product="tablet">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g>. Try on your tablet instead.</string>
+    <string name="app_streaming_blocked_message" product="tablet">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your tablet instead.</string>
     <!-- Message shown when an app is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
-    <string name="app_streaming_blocked_message" product="default">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g>. Try on your phone instead.</string>
-    <!-- Message shown when an app is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
-    <string name="app_streaming_blocked_message_for_permission_dialog" product="tv">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your Android TV device instead.</string>
-    <!-- Message shown when an app is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
-    <string name="app_streaming_blocked_message_for_permission_dialog" product="tablet">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your tablet instead.</string>
-    <!-- Message shown when an app is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
-    <string name="app_streaming_blocked_message_for_permission_dialog" product="default">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your phone instead.</string>
+    <string name="app_streaming_blocked_message" product="default">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g> at this time. Try on your phone instead.</string>
     <!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
     <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tv">This app is requesting additional security. Try on your Android TV device instead.</string>
     <!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
     <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="tablet">This app is requesting additional security. Try on your tablet instead.</string>
     <!-- Message shown when the fingerprint permission is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
     <string name="app_streaming_blocked_message_for_fingerprint_dialog" product="default">This app is requesting additional security. Try on your phone instead.</string>
+    <!-- Message shown when the settings is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
+    <string name="app_streaming_blocked_message_for_settings_dialog" product="tv">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g>. Try on your Android TV device instead.</string>
+    <!-- Message shown when the settings is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
+    <string name="app_streaming_blocked_message_for_settings_dialog" product="tablet">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g>. Try on your tablet instead.</string>
+    <!-- Message shown when the settings is blocked from being streamed to a remote device. [CHAR LIMIT=NONE] -->
+    <string name="app_streaming_blocked_message_for_settings_dialog" product="default">This can’t be accessed on your <xliff:g id="device" example="Chromebook">%1$s</xliff:g>. Try on your phone instead.</string>
 
     <!-- Message displayed in dialog when app is too old to run on this verison of android. [CHAR LIMIT=NONE] -->
     <string name="deprecated_target_sdk_message">This app was built for an older version of Android and may not work properly. Try checking for updates, or contact the developer.</string>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index adff14a..7d41883 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3310,10 +3310,11 @@
   <java-symbol type="string" name="app_streaming_blocked_title_for_camera_dialog" />
   <java-symbol type="string" name="app_streaming_blocked_title_for_fingerprint_dialog" />
   <java-symbol type="string" name="app_streaming_blocked_title_for_microphone_dialog" />
+  <java-symbol type="string" name="app_streaming_blocked_title_for_playstore_dialog" />
   <java-symbol type="string" name="app_streaming_blocked_title_for_settings_dialog" />
   <java-symbol type="string" name="app_streaming_blocked_message" />
-  <java-symbol type="string" name="app_streaming_blocked_message_for_permission_dialog" />
   <java-symbol type="string" name="app_streaming_blocked_message_for_fingerprint_dialog" />
+  <java-symbol type="string" name="app_streaming_blocked_message_for_settings_dialog" />
 
   <!-- Used internally for assistant to launch activity transitions -->
   <java-symbol type="id" name="cross_task_transition" />
diff --git a/core/tests/screenshothelpertests/OWNERS b/core/tests/screenshothelpertests/OWNERS
new file mode 100644
index 0000000..23dc8fb
--- /dev/null
+++ b/core/tests/screenshothelpertests/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 801321
+file:/packages/SystemUI/src/com/android/systemui/screenshot/OWNERS
diff --git a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
index 4b81737..fd4fb13 100644
--- a/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
+++ b/core/tests/screenshothelpertests/src/com/android/internal/util/ScreenshotHelperTest.java
@@ -80,13 +80,14 @@
 
     @Test
     public void testFullscreenScreenshot() {
-        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, false, false, mHandler, null);
+        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
+                WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
     }
 
     @Test
     public void testSelectedRegionScreenshot() {
-        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_SELECTED_REGION, false, false, mHandler,
-                null);
+        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_SELECTED_REGION,
+                WindowManager.ScreenshotSource.SCREENSHOT_OTHER, mHandler, null);
     }
 
     @Test
@@ -101,8 +102,10 @@
         long timeoutMs = 10;
 
         CountDownLatch lock = new CountDownLatch(1);
-        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, false, false, timeoutMs,
+        mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
+                WindowManager.ScreenshotSource.SCREENSHOT_OTHER,
                 mHandler,
+                timeoutMs,
                 uri -> {
                     assertNull(uri);
                     lock.countDown();
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 4080b99..5cba3b4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ShellTaskOrganizer.java
@@ -31,7 +31,6 @@
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
 import android.app.WindowConfiguration;
-import android.content.Context;
 import android.content.LocusId;
 import android.content.pm.ActivityInfo;
 import android.graphics.Rect;
@@ -56,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.ShellInit;
 import com.android.wm.shell.unfold.UnfoldAnimationController;
 
 import java.io.PrintWriter;
@@ -186,41 +186,45 @@
     @Nullable
     private RunningTaskInfo mLastFocusedTaskInfo;
 
-    public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context) {
-        this(null /* taskOrganizerController */, mainExecutor, context, null /* compatUI */,
+    public ShellTaskOrganizer(ShellExecutor mainExecutor) {
+        this(null /* shellInit */, null /* taskOrganizerController */, null /* compatUI */,
                 Optional.empty() /* unfoldAnimationController */,
-                Optional.empty() /* recentTasksController */);
+                Optional.empty() /* recentTasksController */,
+                mainExecutor);
     }
 
-    public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
-            CompatUIController compatUI) {
-        this(null /* taskOrganizerController */, mainExecutor, context, compatUI,
-                Optional.empty() /* unfoldAnimationController */,
-                Optional.empty() /* recentTasksController */);
-    }
-
-    public ShellTaskOrganizer(ShellExecutor mainExecutor, Context context, @Nullable
-            CompatUIController compatUI,
+    public ShellTaskOrganizer(ShellInit shellInit,
+            @Nullable CompatUIController compatUI,
             Optional<UnfoldAnimationController> unfoldAnimationController,
-            Optional<RecentTasksController> recentTasks) {
-        this(null /* taskOrganizerController */, mainExecutor, context, compatUI,
-                unfoldAnimationController, recentTasks);
+            Optional<RecentTasksController> recentTasks,
+            ShellExecutor mainExecutor) {
+        this(shellInit, null /* taskOrganizerController */, compatUI,
+                unfoldAnimationController, recentTasks, mainExecutor);
     }
 
     @VisibleForTesting
-    protected ShellTaskOrganizer(ITaskOrganizerController taskOrganizerController,
-            ShellExecutor mainExecutor, Context context, @Nullable CompatUIController compatUI,
+    protected ShellTaskOrganizer(ShellInit shellInit,
+            ITaskOrganizerController taskOrganizerController,
+            @Nullable CompatUIController compatUI,
             Optional<UnfoldAnimationController> unfoldAnimationController,
-            Optional<RecentTasksController> recentTasks) {
+            Optional<RecentTasksController> recentTasks,
+            ShellExecutor mainExecutor) {
         super(taskOrganizerController, mainExecutor);
         mCompatUI = compatUI;
         mRecentTasks = recentTasks;
         mUnfoldAnimationController = unfoldAnimationController.orElse(null);
-        if (compatUI != null) {
-            compatUI.setCompatUICallback(this);
+        if (shellInit != null) {
+            shellInit.addInitCallback(this::onInit, this);
         }
     }
 
+    private void onInit() {
+        if (mCompatUI != null) {
+            mCompatUI.setCompatUICallback(this);
+        }
+        registerOrganizer();
+    }
+
     @Override
     public List<TaskAppearedInfo> registerOrganizer() {
         synchronized (mLock) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
index 82b0270..b305897 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java
@@ -28,6 +28,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
 /**
@@ -38,13 +39,17 @@
     private final Context mContext;
     private final Transitions mTransitions;
 
-    public ActivityEmbeddingController(Context context, Transitions transitions) {
+    public ActivityEmbeddingController(Context context, ShellInit shellInit,
+            Transitions transitions) {
         mContext = context;
         mTransitions = transitions;
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            shellInit.addInitCallback(this::onInit, this);
+        }
     }
 
     /** Registers to handle transitions. */
-    public void init() {
+    public void onInit() {
         mTransitions.addHandler(this);
     }
 
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 0221122..05fafc5 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
@@ -30,15 +30,20 @@
 import android.graphics.Point;
 import android.graphics.PointF;
 import android.hardware.HardwareBuffer;
+import android.hardware.input.InputManager;
 import android.net.Uri;
 import android.os.Handler;
 import android.os.IBinder;
 import android.os.RemoteException;
+import android.os.SystemClock;
 import android.os.SystemProperties;
 import android.os.UserHandle;
 import android.provider.Settings.Global;
 import android.util.Log;
 import android.view.IWindowFocusObserver;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
@@ -292,6 +297,9 @@
         } else if (keyAction == MotionEvent.ACTION_UP || keyAction == MotionEvent.ACTION_CANCEL) {
             ProtoLog.d(WM_SHELL_BACK_PREVIEW,
                     "Finishing gesture with event action: %d", keyAction);
+            if (keyAction == MotionEvent.ACTION_CANCEL) {
+                mTriggerBack = false;
+            }
             onGestureFinished(true);
         }
     }
@@ -321,7 +329,6 @@
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Received backNavigationInfo:%s", backNavigationInfo);
         if (backNavigationInfo == null) {
             Log.e(TAG, "Received BackNavigationInfo is null.");
-            finishAnimation();
             return;
         }
         int backType = backNavigationInfo.getType();
@@ -397,6 +404,25 @@
         dispatchOnBackProgressed(targetCallback, backEvent);
     }
 
+    private void injectBackKey() {
+        sendBackEvent(KeyEvent.ACTION_DOWN);
+        sendBackEvent(KeyEvent.ACTION_UP);
+    }
+
+    private void sendBackEvent(int action) {
+        final long when = SystemClock.uptimeMillis();
+        final KeyEvent ev = new KeyEvent(when, when, action, KeyEvent.KEYCODE_BACK, 0 /* repeat */,
+                0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
+                KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
+                InputDevice.SOURCE_KEYBOARD);
+
+        ev.setDisplayId(mContext.getDisplay().getDisplayId());
+        if (!InputManager.getInstance()
+                .injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC)) {
+            Log.e(TAG, "Inject input event fail");
+        }
+    }
+
     private void onGestureFinished(boolean fromTouch) {
         ProtoLog.d(WM_SHELL_BACK_PREVIEW, "onGestureFinished() mTriggerBack == %s", mTriggerBack);
         if (fromTouch) {
@@ -405,7 +431,17 @@
             mBackGestureStarted = false;
         }
 
-        if (mTransitionInProgress || mBackNavigationInfo == null) {
+        if (mTransitionInProgress) {
+            return;
+        }
+
+        if (mBackNavigationInfo == null) {
+            // No focus window found or core are running recents animation, inject back key as
+            // legacy behavior.
+            if (mTriggerBack) {
+                injectBackKey();
+            }
+            finishAnimation();
             return;
         }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index d7f1292..3982616 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -70,12 +70,10 @@
 import android.os.UserManager;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.RankingMap;
-import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
 import android.util.Slog;
 import android.util.SparseArray;
-import android.util.SparseSetArray;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.WindowInsets;
@@ -103,14 +101,17 @@
 import com.android.wm.shell.pip.PinnedStackListenerForwarder;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
@@ -177,8 +178,8 @@
     private int mCurrentUserId;
     // Current profiles of the user (e.g. user with a workprofile)
     private SparseArray<UserInfo> mCurrentProfiles;
-    // Saves notification keys of active bubbles when users are switched.
-    private final SparseSetArray<String> mSavedBubbleKeysPerUser;
+    // Saves data about active bubbles when users are switched.
+    private final SparseArray<UserBubbleData> mSavedUserBubbleData;
 
     // Used when ranking updates occur and we check if things should bubble / unbubble
     private NotificationListenerService.Ranking mTmpRanking;
@@ -227,6 +228,7 @@
 
   
     public BubbleController(Context context,
+            ShellInit shellInit,
             ShellController shellController,
             BubbleData data,
             @Nullable BubbleStackView.SurfaceSynchronizer synchronizer,
@@ -271,7 +273,7 @@
         mCurrentUserId = ActivityManager.getCurrentUser();
         mBubblePositioner = positioner;
         mBubbleData = data;
-        mSavedBubbleKeysPerUser = new SparseSetArray<>();
+        mSavedUserBubbleData = new SparseArray<>();
         mBubbleIconFactory = new BubbleIconFactory(context);
         mBubbleBadgeIconFactory = new BubbleBadgeIconFactory(context);
         mDisplayController = displayController;
@@ -279,6 +281,7 @@
         mOneHandedOptional = oneHandedOptional;
         mDragAndDropController = dragAndDropController;
         mSyncQueue = syncQueue;
+        shellInit.addInitCallback(this::onInit, this);
     }
 
     private void registerOneHandedState(OneHandedController oneHanded) {
@@ -300,7 +303,7 @@
                 });
     }
 
-    public void initialize() {
+    protected void onInit() {
         mBubbleData.setListener(mBubbleDataListener);
         mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged);
 
@@ -420,6 +423,13 @@
         List<UserInfo> users = mUserManager.getAliveUsers();
         mDataRepository.sanitizeBubbles(users);
 
+        // Init profiles
+        SparseArray<UserInfo> userProfiles = new SparseArray<>();
+        for (UserInfo user : mUserManager.getProfiles(mCurrentUserId)) {
+            userProfiles.put(user.id, user);
+        }
+        mCurrentProfiles = userProfiles;
+
         mShellController.addConfigurationChangeListener(this);
     }
 
@@ -774,11 +784,13 @@
      */
     private void saveBubbles(@UserIdInt int userId) {
         // First clear any existing keys that might be stored.
-        mSavedBubbleKeysPerUser.remove(userId);
+        mSavedUserBubbleData.remove(userId);
+        UserBubbleData userBubbleData = new UserBubbleData();
         // Add in all active bubbles for the current user.
         for (Bubble bubble : mBubbleData.getBubbles()) {
-            mSavedBubbleKeysPerUser.add(userId, bubble.getKey());
+            userBubbleData.add(bubble.getKey(), bubble.showInShade());
         }
+        mSavedUserBubbleData.put(userId, userBubbleData);
     }
 
     /**
@@ -787,22 +799,23 @@
      * @param userId the id of the user
      */
     private void restoreBubbles(@UserIdInt int userId) {
-        ArraySet<String> savedBubbleKeys = mSavedBubbleKeysPerUser.get(userId);
-        if (savedBubbleKeys == null) {
+        UserBubbleData savedBubbleData = mSavedUserBubbleData.get(userId);
+        if (savedBubbleData == null) {
             // There were no bubbles saved for this used.
             return;
         }
-        mSysuiProxy.getShouldRestoredEntries(savedBubbleKeys, (entries) -> {
+        mSysuiProxy.getShouldRestoredEntries(savedBubbleData.getKeys(), (entries) -> {
             mMainExecutor.execute(() -> {
                 for (BubbleEntry e : entries) {
                     if (canLaunchInTaskView(mContext, e)) {
-                        updateBubble(e, true /* suppressFlyout */, false /* showInShade */);
+                        boolean showInShade = savedBubbleData.isShownInShade(e.getKey());
+                        updateBubble(e, true /* suppressFlyout */, showInShade);
                     }
                 }
             });
         });
         // Finally, remove the entries for this user now that bubbles are restored.
-        mSavedBubbleKeysPerUser.remove(userId);
+        mSavedUserBubbleData.remove(userId);
     }
 
     @Override
@@ -993,7 +1006,19 @@
      */
     @VisibleForTesting
     public void updateBubble(BubbleEntry notif) {
-        updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
+        int bubbleUserId = notif.getStatusBarNotification().getUserId();
+        if (isCurrentProfile(bubbleUserId)) {
+            updateBubble(notif, false /* suppressFlyout */, true /* showInShade */);
+        } else {
+            // Skip update, but store it in user bubbles so it gets restored after user switch
+            mSavedUserBubbleData.get(bubbleUserId, new UserBubbleData()).add(notif.getKey(),
+                    true /* shownInShade */);
+            if (DEBUG_BUBBLE_CONTROLLER) {
+                Log.d(TAG,
+                        "Ignore update to bubble for not active user. Bubble userId=" + bubbleUserId
+                                + " current userId=" + mCurrentUserId);
+            }
+        }
     }
 
     /**
@@ -1842,4 +1867,33 @@
             }
         }
     }
+
+    /**
+     * Bubble data that is stored per user.
+     * Used to store and restore active bubbles during user switching.
+     */
+    private static class UserBubbleData {
+        private final Map<String, Boolean> mKeyToShownInShadeMap = new HashMap<>();
+
+        /**
+         * Add bubble key and whether it should be shown in notification shade
+         */
+        void add(String key, boolean shownInShade) {
+            mKeyToShownInShadeMap.put(key, shownInShade);
+        }
+
+        /**
+         * Get all bubble keys stored for this user
+         */
+        Set<String> getKeys() {
+            return mKeyToShownInShadeMap.keySet();
+        }
+
+        /**
+         * Check if this bubble with the given key should be shown in the notification shade
+         */
+        boolean isShownInShade(String key) {
+            return mKeyToShownInShadeMap.get(key);
+        }
+    }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index f8ccf23..cf792cd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -23,12 +23,10 @@
 
 import android.app.NotificationChannel;
 import android.content.pm.UserInfo;
-import android.content.res.Configuration;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.RankingMap;
-import android.util.ArraySet;
 import android.util.Pair;
 import android.util.SparseArray;
 
@@ -42,6 +40,7 @@
 import java.lang.annotation.Target;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
@@ -284,7 +283,7 @@
 
         void getPendingOrActiveEntry(String key, Consumer<BubbleEntry> callback);
 
-        void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys,
+        void getShouldRestoredEntries(Set<String> savedBubbleKeys,
                 Consumer<List<BubbleEntry>> callback);
 
         void setNotificationInterruption(String key);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
index 28c7367..ae1f433 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayChangeController.java
@@ -28,6 +28,7 @@
 import androidx.annotation.BinderThread;
 
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.concurrent.CopyOnWriteArrayList;
 
@@ -47,10 +48,15 @@
     private final CopyOnWriteArrayList<OnDisplayChangingListener> mDisplayChangeListener =
             new CopyOnWriteArrayList<>();
 
-    public DisplayChangeController(IWindowManager wmService, ShellExecutor mainExecutor) {
+    public DisplayChangeController(IWindowManager wmService, ShellInit shellInit,
+            ShellExecutor mainExecutor) {
         mMainExecutor = mainExecutor;
         mWmService = wmService;
         mControllerImpl = new DisplayChangeWindowControllerImpl();
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
         try {
             mWmService.setDisplayChangeWindowController(mControllerImpl);
         } catch (RemoteException e) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
index 764936c..f07ea75 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java
@@ -34,6 +34,7 @@
 
 import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener;
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -57,19 +58,23 @@
     private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>();
     private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>();
 
-    public DisplayController(Context context, IWindowManager wmService,
+    public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit,
             ShellExecutor mainExecutor) {
         mMainExecutor = mainExecutor;
         mContext = context;
         mWmService = wmService;
-        mChangeController = new DisplayChangeController(mWmService, mainExecutor);
+        // TODO: Inject this instead
+        mChangeController = new DisplayChangeController(mWmService, shellInit, mainExecutor);
         mDisplayContainerListener = new DisplayWindowListenerImpl();
+        // Note, add this after DisplaceChangeController is constructed to ensure that is
+        // initialized first
+        shellInit.addInitCallback(this::onInit, this);
     }
 
     /**
      * Initializes the window listener.
      */
-    public void initialize() {
+    public void onInit() {
         try {
             int[] displayIds = mWmService.registerDisplayWindowListener(mDisplayContainerListener);
             for (int i = 0; i < displayIds.length; i++) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index b3f6247..266cf29 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -44,6 +44,7 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.view.IInputMethodManager;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
 import java.util.concurrent.Executor;
@@ -74,18 +75,24 @@
     private final ArrayList<ImePositionProcessor> mPositionProcessors = new ArrayList<>();
 
 
-    public DisplayImeController(IWindowManager wmService, DisplayController displayController,
+    public DisplayImeController(IWindowManager wmService,
+            ShellInit shellInit,
+            DisplayController displayController,
             DisplayInsetsController displayInsetsController,
-            Executor mainExecutor, TransactionPool transactionPool) {
+            TransactionPool transactionPool,
+            Executor mainExecutor) {
         mWmService = wmService;
         mDisplayController = displayController;
         mDisplayInsetsController = displayInsetsController;
         mMainExecutor = mainExecutor;
         mTransactionPool = transactionPool;
+        shellInit.addInitCallback(this::onInit, this);
     }
 
-    /** Starts monitor displays changes and set insets controller for each displays. */
-    public void startMonitorDisplays() {
+    /**
+     * Starts monitor displays changes and set insets controller for each displays.
+     */
+    public void onInit() {
         mDisplayController.addDisplayWindowListener(this);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
index f546f11..90a01f8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayInsetsController.java
@@ -29,6 +29,7 @@
 import androidx.annotation.BinderThread;
 
 import com.android.wm.shell.common.annotations.ShellMainThread;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.concurrent.CopyOnWriteArrayList;
 
@@ -45,17 +46,20 @@
     private final SparseArray<CopyOnWriteArrayList<OnInsetsChangedListener>> mListeners =
             new SparseArray<>();
 
-    public DisplayInsetsController(IWindowManager wmService, DisplayController displayController,
+    public DisplayInsetsController(IWindowManager wmService,
+            ShellInit shellInit,
+            DisplayController displayController,
             ShellExecutor mainExecutor) {
         mWmService = wmService;
         mDisplayController = displayController;
         mMainExecutor = mainExecutor;
+        shellInit.addInitCallback(this::onInit, this);
     }
 
     /**
      * Starts listening for insets for each display.
      **/
-    public void initialize() {
+    public void onInit() {
         mDisplayController.addDisplayWindowListener(this);
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java
index 806f795..10b121b 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/DynamicOverride.java
@@ -92,6 +92,8 @@
  *
  * For example, this uses the same setup as above, but the interface provided (if bound) is used
  * otherwise the default is created:
+ *
+ * BaseModule:
  *   @BindsOptionalOf
  *   @DynamicOverride
  *   abstract Interface dynamicInterface();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java
new file mode 100644
index 0000000..482b199
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTrigger.java
@@ -0,0 +1,38 @@
+/*
+ * 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.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * An annotation to specifically mark the provider that is triggering the creation of independent
+ * shell components that are not created as a part of the dependencies for interfaces passed to
+ * SysUI.
+ *
+ * TODO: This will be removed once we have a more explicit method for specifying components to start
+ *       with SysUI
+ */
+@Documented
+@Inherited
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ShellCreateTrigger {}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java
new file mode 100644
index 0000000..31c6789
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/ShellCreateTriggerOverride.java
@@ -0,0 +1,38 @@
+/*
+ * 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.dagger;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import javax.inject.Qualifier;
+
+/**
+ * An annotation for non-base modules to specifically mark the provider that is triggering the
+ * creation of independent shell components that are not created as a part of the dependencies for
+ * interfaces passed to SysUI.
+ *
+ * TODO: This will be removed once we have a more explicit method for specifying components to start
+ *       with SysUI
+ */
+@Documented
+@Inherited
+@Qualifier
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ShellCreateTriggerOverride {}
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 a168cb2..3add417 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
@@ -112,16 +112,20 @@
     @WMSingleton
     @Provides
     static DisplayController provideDisplayController(Context context,
-            IWindowManager wmService, @ShellMainThread ShellExecutor mainExecutor) {
-        return new DisplayController(context, wmService, mainExecutor);
+            IWindowManager wmService,
+            ShellInit shellInit,
+            @ShellMainThread ShellExecutor mainExecutor) {
+        return new DisplayController(context, wmService, shellInit, mainExecutor);
     }
 
     @WMSingleton
     @Provides
-    static DisplayInsetsController provideDisplayInsetsController( IWindowManager wmService,
+    static DisplayInsetsController provideDisplayInsetsController(IWindowManager wmService,
+            ShellInit shellInit,
             DisplayController displayController,
             @ShellMainThread ShellExecutor mainExecutor) {
-        return new DisplayInsetsController(wmService, displayController, mainExecutor);
+        return new DisplayInsetsController(wmService, shellInit, displayController,
+                mainExecutor);
     }
 
     // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
@@ -134,16 +138,17 @@
     static DisplayImeController provideDisplayImeController(
             @DynamicOverride Optional<DisplayImeController> overrideDisplayImeController,
             IWindowManager wmService,
+            ShellInit shellInit,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
-            @ShellMainThread ShellExecutor mainExecutor,
-            TransactionPool transactionPool
+            TransactionPool transactionPool,
+            @ShellMainThread ShellExecutor mainExecutor
     ) {
         if (overrideDisplayImeController.isPresent()) {
             return overrideDisplayImeController.get();
         }
-        return new DisplayImeController(wmService, displayController, displayInsetsController,
-                mainExecutor, transactionPool);
+        return new DisplayImeController(wmService, shellInit, displayController,
+                displayInsetsController, transactionPool, mainExecutor);
     }
 
     @WMSingleton
@@ -155,42 +160,45 @@
     @WMSingleton
     @Provides
     static DragAndDropController provideDragAndDropController(Context context,
+            ShellInit shellInit,
             ShellController shellController,
             DisplayController displayController,
             UiEventLogger uiEventLogger,
             IconProvider iconProvider,
             @ShellMainThread ShellExecutor mainExecutor) {
-        return new DragAndDropController(context, shellController, displayController, uiEventLogger,
-                iconProvider, mainExecutor);
+        return new DragAndDropController(context, shellInit, shellController, displayController,
+                uiEventLogger, iconProvider, mainExecutor);
     }
 
     @WMSingleton
     @Provides
-    static ShellTaskOrganizer provideShellTaskOrganizer(@ShellMainThread ShellExecutor mainExecutor,
-            Context context,
+    static ShellTaskOrganizer provideShellTaskOrganizer(
+            ShellInit shellInit,
             CompatUIController compatUI,
             Optional<UnfoldAnimationController> unfoldAnimationController,
-            Optional<RecentTasksController> recentTasksOptional
+            Optional<RecentTasksController> recentTasksOptional,
+            @ShellMainThread ShellExecutor mainExecutor
     ) {
-        return new ShellTaskOrganizer(mainExecutor, context, compatUI, unfoldAnimationController,
-                recentTasksOptional);
+        return new ShellTaskOrganizer(shellInit, compatUI, unfoldAnimationController,
+                recentTasksOptional, mainExecutor);
     }
 
     @WMSingleton
     @Provides
     static KidsModeTaskOrganizer provideKidsModeTaskOrganizer(
-            @ShellMainThread ShellExecutor mainExecutor,
-            @ShellMainThread Handler mainHandler,
             Context context,
+            ShellInit shellInit,
             SyncTransactionQueue syncTransactionQueue,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
             Optional<UnfoldAnimationController> unfoldAnimationController,
-            Optional<RecentTasksController> recentTasksOptional
+            Optional<RecentTasksController> recentTasksOptional,
+            @ShellMainThread ShellExecutor mainExecutor,
+            @ShellMainThread Handler mainHandler
     ) {
-        return new KidsModeTaskOrganizer(mainExecutor, mainHandler, context, syncTransactionQueue,
+        return new KidsModeTaskOrganizer(context, shellInit, syncTransactionQueue,
                 displayController, displayInsetsController, unfoldAnimationController,
-                recentTasksOptional);
+                recentTasksOptional, mainExecutor, mainHandler);
     }
 
     @WMSingleton
@@ -256,6 +264,20 @@
         return backAnimationController.map(BackAnimationController::getBackAnimationImpl);
     }
 
+    @WMSingleton
+    @Provides
+    static Optional<BackAnimationController> provideBackAnimationController(
+            Context context,
+            @ShellMainThread ShellExecutor shellExecutor,
+            @ShellBackgroundThread Handler backgroundHandler
+    ) {
+        if (BackAnimationController.IS_ENABLED) {
+            return Optional.of(
+                    new BackAnimationController(shellExecutor, backgroundHandler, context));
+        }
+        return Optional.empty();
+    }
+
     //
     // Bubbles (optional feature)
     //
@@ -282,12 +304,15 @@
     @Provides
     static FullscreenTaskListener provideFullscreenTaskListener(
             @DynamicOverride Optional<FullscreenTaskListener> fullscreenTaskListener,
+            ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
             SyncTransactionQueue syncQueue,
             Optional<RecentTasksController> recentTasksOptional) {
         if (fullscreenTaskListener.isPresent()) {
             return fullscreenTaskListener.get();
         } else {
-            return new FullscreenTaskListener(syncQueue, recentTasksOptional);
+            return new FullscreenTaskListener(shellInit, shellTaskOrganizer, syncQueue,
+                    recentTasksOptional);
         }
     }
 
@@ -343,7 +368,7 @@
 
     @WMSingleton
     @Provides
-    static Optional<FreeformComponents> provideFreeformTaskListener(
+    static Optional<FreeformComponents> provideFreeformComponents(
             @DynamicOverride Optional<FreeformComponents> freeformComponents,
             Context context) {
         if (FreeformComponents.isFreeformEnabled(context)) {
@@ -440,11 +465,13 @@
     @Provides
     static Optional<RecentTasksController> provideRecentTasksController(
             Context context,
+            ShellInit shellInit,
             TaskStackListenerImpl taskStackListener,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
         return Optional.ofNullable(
-                RecentTasksController.create(context, taskStackListener, mainExecutor));
+                RecentTasksController.create(context, shellInit, taskStackListener,
+                        mainExecutor));
     }
 
     //
@@ -459,12 +486,15 @@
 
     @WMSingleton
     @Provides
-    static Transitions provideTransitions(ShellTaskOrganizer organizer, TransactionPool pool,
-            DisplayController displayController, Context context,
+    static Transitions provideTransitions(Context context,
+            ShellInit shellInit,
+            ShellTaskOrganizer organizer,
+            TransactionPool pool,
+            DisplayController displayController,
             @ShellMainThread ShellExecutor mainExecutor,
             @ShellMainThread Handler mainHandler,
             @ShellAnimationThread ShellExecutor animExecutor) {
-        return new Transitions(organizer, pool, displayController, context, mainExecutor,
+        return new Transitions(context, shellInit, organizer, pool, displayController, mainExecutor,
                 mainHandler, animExecutor);
     }
 
@@ -542,11 +572,13 @@
     @WMSingleton
     @Provides
     static StartingWindowController provideStartingWindowController(Context context,
+            ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
             @ShellSplashscreenThread ShellExecutor splashScreenExecutor,
             StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider,
             TransactionPool pool) {
-        return new StartingWindowController(context, splashScreenExecutor,
-                startingWindowTypeAlgorithm, iconProvider, pool);
+        return new StartingWindowController(context, shellInit, shellTaskOrganizer,
+                splashScreenExecutor, startingWindowTypeAlgorithm, iconProvider, pool);
     }
 
     // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
@@ -595,9 +627,11 @@
 
     @WMSingleton
     @Provides
-    static Optional<ActivityEmbeddingController> provideActivityEmbeddingController(
-            Context context, Transitions transitions) {
-        return Optional.of(new ActivityEmbeddingController(context, transitions));
+    static ActivityEmbeddingController provideActivityEmbeddingController(
+            Context context,
+            ShellInit shellInit,
+            Transitions transitions) {
+        return new ActivityEmbeddingController(context, shellInit, transitions);
     }
 
     //
@@ -606,24 +640,34 @@
 
     @WMSingleton
     @Provides
-    static ShellInterface provideShellSysuiCallbacks(ShellController shellController) {
+    static ShellInterface provideShellSysuiCallbacks(
+            @ShellCreateTrigger Object createTrigger,
+            ShellController shellController) {
         return shellController.asShell();
     }
 
     @WMSingleton
     @Provides
-    static ShellController provideShellController(@ShellMainThread ShellExecutor mainExecutor) {
-        return new ShellController(mainExecutor);
+    static ShellController provideShellController(ShellInit shellInit,
+            @ShellMainThread ShellExecutor mainExecutor) {
+        return new ShellController(shellInit, mainExecutor);
     }
 
     //
     // Misc
     //
 
+    // Workaround for dynamic overriding with a default implementation, see {@link DynamicOverride}
+    @BindsOptionalOf
+    @ShellCreateTriggerOverride
+    abstract Object provideIndependentShellComponentsToCreateOverride();
+
+    // TODO: Temporarily move dependencies to this instead of ShellInit since that is needed to add
+    // the callback. We will be moving to a different explicit startup mechanism in a follow- up CL.
     @WMSingleton
+    @ShellCreateTrigger
     @Provides
-    static ShellInit provideShellInitImpl(
-            ShellController shellController,
+    static Object provideIndependentShellComponentsToCreate(
             DisplayController displayController,
             DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController,
@@ -638,29 +682,17 @@
             Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
             Optional<FreeformComponents> freeformComponents,
             Optional<RecentTasksController> recentTasksOptional,
-            Optional<ActivityEmbeddingController> activityEmbeddingOptional,
+            ActivityEmbeddingController activityEmbeddingOptional,
             Transitions transitions,
             StartingWindowController startingWindow,
-            @ShellMainThread ShellExecutor mainExecutor) {
-        return new ShellInit(shellController,
-                displayController,
-                displayImeController,
-                displayInsetsController,
-                dragAndDropController,
-                shellTaskOrganizer,
-                kidsModeTaskOrganizer,
-                bubblesOptional,
-                splitScreenOptional,
-                pipTouchHandlerOptional,
-                fullscreenTaskListener,
-                unfoldAnimationController,
-                unfoldTransitionHandler,
-                freeformComponents,
-                recentTasksOptional,
-                activityEmbeddingOptional,
-                transitions,
-                startingWindow,
-                mainExecutor);
+            @ShellCreateTriggerOverride Optional<Object> overriddenCreateTrigger) {
+        return new Object();
+    }
+
+    @WMSingleton
+    @Provides
+    static ShellInit provideShellInit(@ShellMainThread ShellExecutor mainExecutor) {
+        return new ShellInit(mainExecutor);
     }
 
     @WMSingleton
@@ -679,18 +711,4 @@
                 kidsModeTaskOrganizer, splitScreenOptional, pipOptional, oneHandedOptional,
                 hideDisplayCutout, recentTasksOptional, mainExecutor);
     }
-
-    @WMSingleton
-    @Provides
-    static Optional<BackAnimationController> provideBackAnimationController(
-            Context context,
-            @ShellMainThread ShellExecutor shellExecutor,
-            @ShellBackgroundThread Handler backgroundHandler
-    ) {
-        if (BackAnimationController.IS_ENABLED) {
-            return Optional.of(
-                    new BackAnimationController(shellExecutor, backgroundHandler, context));
-        }
-        return Optional.empty();
-    }
 }
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 588a1aa..e2bf767 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
@@ -75,6 +75,8 @@
 import com.android.wm.shell.recents.RecentTasksController;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.SplitscreenPipMixedHandler;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider;
 import com.android.wm.shell.unfold.UnfoldAnimationController;
@@ -138,6 +140,7 @@
     @WMSingleton
     @Provides
     static BubbleController provideBubbleController(Context context,
+            ShellInit shellInit,
             ShellController shellController,
             BubbleData data,
             FloatingContentCoordinator floatingContentCoordinator,
@@ -158,8 +161,8 @@
             @ShellBackgroundThread ShellExecutor bgExecutor,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
-        return new BubbleController(context, shellController, data, null /* synchronizer */,
-                floatingContentCoordinator,
+        return new BubbleController(context, shellInit, shellController, data,
+                null /* synchronizer */, floatingContentCoordinator,
                 new BubbleDataRepository(context, launcherApps, mainExecutor),
                 statusBarService, windowManager, windowManagerShellWrapper, userManager,
                 launcherApps, logger, taskStackListener, organizer, positioner, displayController,
@@ -205,18 +208,34 @@
     @WMSingleton
     @Provides
     static FreeformTaskListener<?> provideFreeformTaskListener(
+            Context context,
+            ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
             WindowDecorViewModel<?> windowDecorViewModel) {
-        return new FreeformTaskListener<>(windowDecorViewModel);
+        // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
+        //                    override for this controller from the base module
+        ShellInit init = FreeformComponents.isFreeformEnabled(context)
+                ? shellInit
+                : null;
+        return new FreeformTaskListener<>(init, shellTaskOrganizer,
+                windowDecorViewModel);
     }
 
     @WMSingleton
     @Provides
-    static FreeformTaskTransitionHandler provideTaskTransitionHandler(
+    static FreeformTaskTransitionHandler provideFreeformTaskTransitionHandler(
+            Context context,
+            ShellInit shellInit,
             Transitions transitions,
             WindowDecorViewModel<?> windowDecorViewModel,
             FreeformTaskListener<?> freeformTaskListener) {
-        return new FreeformTaskTransitionHandler(transitions, windowDecorViewModel,
-                freeformTaskListener);
+        // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
+        //                    override for this controller from the base module
+        ShellInit init = FreeformComponents.isFreeformEnabled(context)
+                ? shellInit
+                : null;
+        return new FreeformTaskTransitionHandler(init, transitions,
+                windowDecorViewModel, freeformTaskListener);
     }
 
     //
@@ -247,20 +266,25 @@
     @Provides
     @DynamicOverride
     static SplitScreenController provideSplitScreenController(
+            Context context,
+            ShellInit shellInit,
             ShellController shellController,
             ShellTaskOrganizer shellTaskOrganizer,
-            SyncTransactionQueue syncQueue, Context context,
+            SyncTransactionQueue syncQueue,
             RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer,
-            @ShellMainThread ShellExecutor mainExecutor,
             DisplayController displayController,
             DisplayImeController displayImeController,
-            DisplayInsetsController displayInsetsController, Transitions transitions,
-            TransactionPool transactionPool, IconProvider iconProvider,
-            Optional<RecentTasksController> recentTasks) {
-        return new SplitScreenController(shellController, shellTaskOrganizer, syncQueue, context,
-                rootTaskDisplayAreaOrganizer, mainExecutor, displayController, displayImeController,
-                displayInsetsController, transitions, transactionPool, iconProvider,
-                recentTasks);
+            DisplayInsetsController displayInsetsController,
+            DragAndDropController dragAndDropController,
+            Transitions transitions,
+            TransactionPool transactionPool,
+            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);
     }
 
     //
@@ -332,14 +356,16 @@
     @WMSingleton
     @Provides
     static PipTouchHandler providePipTouchHandler(Context context,
-            PhonePipMenuController menuPhoneController, PipBoundsAlgorithm pipBoundsAlgorithm,
+            ShellInit shellInit,
+            PhonePipMenuController menuPhoneController,
+            PipBoundsAlgorithm pipBoundsAlgorithm,
             PipBoundsState pipBoundsState,
             PipTaskOrganizer pipTaskOrganizer,
             PipMotionHelper pipMotionHelper,
             FloatingContentCoordinator floatingContentCoordinator,
             PipUiEventLogger pipUiEventLogger,
             @ShellMainThread ShellExecutor mainExecutor) {
-        return new PipTouchHandler(context, menuPhoneController, pipBoundsAlgorithm,
+        return new PipTouchHandler(context, shellInit, menuPhoneController, pipBoundsAlgorithm,
                 pipBoundsState, pipTaskOrganizer, pipMotionHelper,
                 floatingContentCoordinator, pipUiEventLogger, mainExecutor);
     }
@@ -414,9 +440,31 @@
                 floatingContentCoordinator);
     }
 
+    @WMSingleton
+    @Provides
+    static PipParamsChangedForwarder providePipParamsChangedForwarder() {
+        return new PipParamsChangedForwarder();
+    }
+
+    //
+    // Transitions
+    //
+
+    @WMSingleton
+    @Provides
+    static SplitscreenPipMixedHandler provideSplitscreenPipMixedHandler(
+            ShellInit shellInit,
+            Optional<SplitScreenController> splitScreenOptional,
+            Optional<PipTouchHandler> pipTouchHandlerOptional,
+            Transitions transitions) {
+        return new SplitscreenPipMixedHandler(shellInit, splitScreenOptional,
+                pipTouchHandlerOptional, transitions);
+    }
+
     //
     // Unfold transition
     //
+
     @WMSingleton
     @Provides
     @DynamicOverride
@@ -426,6 +474,7 @@
             @UnfoldTransition SplitTaskUnfoldAnimator splitAnimator,
             FullscreenUnfoldTaskAnimator fullscreenAnimator,
             Lazy<Optional<UnfoldTransitionHandler>> unfoldTransitionHandler,
+            ShellInit shellInit,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
         final List<UnfoldTaskAnimator> animators = new ArrayList<>();
@@ -433,6 +482,7 @@
         animators.add(fullscreenAnimator);
 
         return new UnfoldAnimationController(
+                        shellInit,
                         transactionPool,
                         progressProvider.get(),
                         animators,
@@ -441,7 +491,6 @@
                 );
     }
 
-
     @Provides
     static FullscreenUnfoldTaskAnimator provideFullscreenUnfoldTaskAnimator(
             Context context,
@@ -460,6 +509,10 @@
             Lazy<Optional<SplitScreenController>> splitScreenOptional,
             DisplayInsetsController displayInsetsController
     ) {
+        // TODO(b/238217847): The lazy reference here causes some dependency issues since it
+        // immediately registers a listener on that controller on init.  We should reference the
+        // controller directly once we refactor ShellTaskOrganizer to not depend on the unfold
+        // animation controller directly.
         return new SplitTaskUnfoldAnimator(context, executor, splitScreenOptional,
                 backgroundController, displayInsetsController);
     }
@@ -485,8 +538,9 @@
             @UnfoldShellTransition SplitTaskUnfoldAnimator unfoldAnimator,
             TransactionPool transactionPool,
             Transitions transitions,
-            @ShellMainThread ShellExecutor executor) {
-        return new UnfoldTransitionHandler(progressProvider.get(), animator,
+            @ShellMainThread ShellExecutor executor,
+            ShellInit shellInit) {
+        return new UnfoldTransitionHandler(shellInit, progressProvider.get(), animator,
                 unfoldAnimator, transactionPool, executor, transitions);
     }
 
@@ -502,9 +556,17 @@
         );
     }
 
+    //
+    // Misc
+    //
+
+    // TODO: Temporarily move dependencies to this instead of ShellInit since that is needed to add
+    // the callback. We will be moving to a different explicit startup mechanism in a follow- up CL.
     @WMSingleton
+    @ShellCreateTriggerOverride
     @Provides
-    static PipParamsChangedForwarder providePipParamsChangedForwarder() {
-        return new PipParamsChangedForwarder();
+    static Object provideIndependentShellComponentsToCreate(
+            SplitscreenPipMixedHandler splitscreenPipMixedHandler) {
+        return new Object();
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
index f4e2f20..2aa933d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md
@@ -44,10 +44,24 @@
 
 ### Component initialization
 To initialize the component:
-- On the Shell side, update `ShellInitImpl` to get a signal to initialize when the SysUI is started
+- On the Shell side, you potentially need to do two things to initialize the component:
+  - Inject `ShellInit` into your component and add an init callback
+  - Ensure that your component is a part of the dagger dependency graph, either by:
+    - Making this component a dependency of an existing component already exposed to SystemUI
+    - Explicitly add this component to the WMShellBaseModule @ShellCreateTrigger provider or
+      the @ShellCreateTriggerOverride provider for your product module to expose it explicitly 
+      if it is a completely independent component
 - On the SysUI side, update `WMShell` to setup any bindings for the component that depend on
   SysUI code
 
+To verify that your component is being initialized at startup, you can enable the `WM_SHELL_INIT` 
+protolog group and restart the SysUI process:
+```shell
+adb shell wm logging enable-text WM_SHELL_INIT
+adb shell kill `pid com.android.systemui`
+adb logcat *:S WindowManagerShell
+```
+
 ### General Do's & Dont's
 Do:
 - Do add unit tests for all new components
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
index c5df53b..4697a01 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java
@@ -62,9 +62,9 @@
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ConfigurationChangeListener;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
-import java.util.Optional;
 
 /**
  * Handles the global drag and drop handling for the Shell.
@@ -94,6 +94,7 @@
     }
 
     public DragAndDropController(Context context,
+            ShellInit shellInit,
             ShellController shellController,
             DisplayController displayController,
             UiEventLogger uiEventLogger,
@@ -105,14 +106,29 @@
         mLogger = new DragAndDropEventLogger(uiEventLogger);
         mIconProvider = iconProvider;
         mMainExecutor = mainExecutor;
+        shellInit.addInitCallback(this::onInit, this);
     }
 
-    public void initialize(Optional<SplitScreenController> splitscreen) {
-        mSplitScreen = splitscreen.orElse(null);
-        mDisplayController.addDisplayWindowListener(this);
+    /**
+     * Called when the controller is initialized.
+     */
+    public void onInit() {
+        // TODO(b/238217847): The dependency from SplitscreenController on DragAndDropController is
+        // inverted, which leads to SplitscreenController not setting its instance until after
+        // onDisplayAdded.  We can remove this post once we fix that dependency.
+        mMainExecutor.executeDelayed(() -> {
+            mDisplayController.addDisplayWindowListener(this);
+        }, 0);
         mShellController.addConfigurationChangeListener(this);
     }
 
+    /**
+     * Sets the splitscreen controller to use if the feature is available.
+     */
+    public void setSplitScreenController(SplitScreenController splitscreen) {
+        mSplitScreen = splitscreen;
+    }
+
     /** Adds a listener to be notified of drag and drop events. */
     public void addListener(DragAndDropListener listener) {
         mListeners.add(listener);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
index ba093a5..ab66107 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java
@@ -16,6 +16,8 @@
 
 package com.android.wm.shell.freeform;
 
+import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM;
+
 import android.app.ActivityManager.RunningTaskInfo;
 import android.util.Log;
 import android.util.SparseArray;
@@ -27,6 +29,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
@@ -42,6 +45,7 @@
         implements ShellTaskOrganizer.TaskListener {
     private static final String TAG = "FreeformTaskListener";
 
+    private final ShellTaskOrganizer mShellTaskOrganizer;
     private final WindowDecorViewModel<T> mWindowDecorationViewModel;
 
     private final SparseArray<State<T>> mTasks = new SparseArray<>();
@@ -53,8 +57,19 @@
         T mWindowDecoration;
     }
 
-    public FreeformTaskListener(WindowDecorViewModel<T> windowDecorationViewModel) {
+    public FreeformTaskListener(
+            ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            WindowDecorViewModel<T> windowDecorationViewModel) {
+        mShellTaskOrganizer = shellTaskOrganizer;
         mWindowDecorationViewModel = windowDecorationViewModel;
+        if (shellInit != null) {
+            shellInit.addInitCallback(this::onInit, this);
+        }
+    }
+
+    private void onInit() {
+        mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FREEFORM);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
index 8731eb6..20d7725 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java
@@ -32,6 +32,7 @@
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.windowdecor.WindowDecorViewModel;
 
@@ -49,17 +50,26 @@
 
     private final Transitions mTransitions;
     private final FreeformTaskListener<?> mFreeformTaskListener;
+    private final WindowDecorViewModel<?> mWindowDecorViewModel;
 
     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
 
     public FreeformTaskTransitionHandler(
+            ShellInit shellInit,
             Transitions transitions,
             WindowDecorViewModel<?> windowDecorViewModel,
             FreeformTaskListener<?> freeformTaskListener) {
         mTransitions = transitions;
         mFreeformTaskListener = freeformTaskListener;
+        mWindowDecorViewModel = windowDecorViewModel;
+        if (shellInit != null && Transitions.ENABLE_SHELL_TRANSITIONS) {
+            shellInit.addInitCallback(this::onInit, this);
+        }
+    }
 
-        windowDecorViewModel.setFreeformTaskTransitionStarter(this);
+    private void onInit() {
+        mWindowDecorViewModel.setFreeformTaskTransitionStarter(this);
+        mTransitions.addHandler(this);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
index 79e363b..0ba4afc 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java
@@ -32,6 +32,7 @@
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.recents.RecentTasksController;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
 import java.io.PrintWriter;
@@ -43,19 +44,34 @@
 public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener {
     private static final String TAG = "FullscreenTaskListener";
 
+    private final ShellTaskOrganizer mShellTaskOrganizer;
     private final SyncTransactionQueue mSyncQueue;
     private final Optional<RecentTasksController> mRecentTasksOptional;
 
     private final SparseArray<TaskData> mDataByTaskId = new SparseArray<>();
 
+    /**
+     * This constructor is used by downstream products.
+     */
     public FullscreenTaskListener(SyncTransactionQueue syncQueue) {
-        this(syncQueue, Optional.empty());
+        this(null /* shellInit */, null /* shellTaskOrganizer */, syncQueue, Optional.empty());
     }
 
-    public FullscreenTaskListener(SyncTransactionQueue syncQueue,
-            Optional<RecentTasksController> recentTasks) {
+    public FullscreenTaskListener(ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            SyncTransactionQueue syncQueue,
+            Optional<RecentTasksController> recentTasksOptional) {
+        mShellTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
-        mRecentTasksOptional = recentTasks;
+        mRecentTasksOptional = recentTasksOptional;
+        // Note: Some derivative FullscreenTaskListener implementations do not use ShellInit
+        if (shellInit != null) {
+            shellInit.addInitCallback(this::onInit, this);
+        }
+    }
+
+    private void onInit() {
+        mShellTaskOrganizer.addListenerForType(this, TASK_LISTENER_TYPE_FULLSCREEN);
     }
 
     @Override
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 2c8ba09..73b9b89 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,7 +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.startingsurface.StartingWindowController;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.unfold.UnfoldAnimationController;
 
 import java.io.PrintWriter;
@@ -140,18 +140,18 @@
 
     @VisibleForTesting
     KidsModeTaskOrganizer(
-            ITaskOrganizerController taskOrganizerController,
-            ShellExecutor mainExecutor,
-            Handler mainHandler,
             Context context,
+            ITaskOrganizerController taskOrganizerController,
             SyncTransactionQueue syncTransactionQueue,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
             Optional<UnfoldAnimationController> unfoldAnimationController,
             Optional<RecentTasksController> recentTasks,
-            KidsModeSettingsObserver kidsModeSettingsObserver) {
-        super(taskOrganizerController, mainExecutor, context, /* compatUI= */ null,
-                unfoldAnimationController, recentTasks);
+            KidsModeSettingsObserver kidsModeSettingsObserver,
+            ShellExecutor mainExecutor,
+            Handler mainHandler) {
+        super(/* shellInit= */ null, taskOrganizerController, /* compatUI= */ null,
+                unfoldAnimationController, recentTasks, mainExecutor);
         mContext = context;
         mMainHandler = mainHandler;
         mSyncQueue = syncTransactionQueue;
@@ -161,27 +161,30 @@
     }
 
     public KidsModeTaskOrganizer(
-            ShellExecutor mainExecutor,
-            Handler mainHandler,
             Context context,
+            ShellInit shellInit,
             SyncTransactionQueue syncTransactionQueue,
             DisplayController displayController,
             DisplayInsetsController displayInsetsController,
             Optional<UnfoldAnimationController> unfoldAnimationController,
-            Optional<RecentTasksController> recentTasks) {
-        super(mainExecutor, context, /* compatUI= */ null, unfoldAnimationController, recentTasks);
+            Optional<RecentTasksController> recentTasks,
+            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);
         mContext = context;
         mMainHandler = mainHandler;
         mSyncQueue = syncTransactionQueue;
         mDisplayController = displayController;
         mDisplayInsetsController = displayInsetsController;
+        shellInit.addInitCallback(this::onInit, this);
     }
 
     /**
      * Initializes kids mode status.
      */
-    public void initialize(StartingWindowController startingWindowController) {
-        initStartingWindow(startingWindowController);
+    public void onInit() {
         if (mKidsModeSettingsObserver == null) {
             mKidsModeSettingsObserver = new KidsModeSettingsObserver(mMainHandler, mContext);
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index a2ff972..c86c136 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -57,6 +57,7 @@
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipUiEventLogger;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.io.PrintWriter;
 
@@ -165,6 +166,7 @@
 
     @SuppressLint("InflateParams")
     public PipTouchHandler(Context context,
+            ShellInit shellInit,
             PhonePipMenuController menuController,
             PipBoundsAlgorithm pipBoundsAlgorithm,
             @NonNull PipBoundsState pipBoundsState,
@@ -173,7 +175,6 @@
             FloatingContentCoordinator floatingContentCoordinator,
             PipUiEventLogger pipUiEventLogger,
             ShellExecutor mainExecutor) {
-        // Initialize the Pip input consumer
         mContext = context;
         mMainExecutor = mainExecutor;
         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
@@ -213,9 +214,17 @@
                 mMotionHelper, pipTaskOrganizer, mPipBoundsAlgorithm.getSnapAlgorithm(),
                 this::onAccessibilityShowMenu, this::updateMovementBounds,
                 this::animateToUnStashedState, mainExecutor);
+
+        // TODO(b/181599115): This should really be initializes as part of the pip controller, but
+        // until all PIP implementations derive from the controller, just initialize the touch handler
+        // if it is needed
+        shellInit.addInitCallback(this::onInit, this);
     }
 
-    public void init() {
+    /**
+     * Called when the touch handler is initialized.
+     */
+    public void onInit() {
         Resources res = mContext.getResources();
         mEnableResize = res.getBoolean(R.bool.config_pipEnableResizeForMenu);
         reloadResources();
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 49042e6..3d1a7e9 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.ShellInit;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.SplitBounds;
 
@@ -85,28 +86,32 @@
     @Nullable
     public static RecentTasksController create(
             Context context,
+            ShellInit shellInit,
             TaskStackListenerImpl taskStackListener,
             @ShellMainThread ShellExecutor mainExecutor
     ) {
         if (!context.getResources().getBoolean(com.android.internal.R.bool.config_hasRecents)) {
             return null;
         }
-        return new RecentTasksController(context, taskStackListener, mainExecutor);
+        return new RecentTasksController(context, shellInit, taskStackListener, mainExecutor);
     }
 
-    RecentTasksController(Context context, TaskStackListenerImpl taskStackListener,
+    RecentTasksController(Context context,
+            ShellInit shellInit,
+            TaskStackListenerImpl taskStackListener,
             ShellExecutor mainExecutor) {
         mContext = context;
         mIsDesktopMode = mContext.getPackageManager().hasSystemFeature(FEATURE_PC);
         mTaskStackListener = taskStackListener;
         mMainExecutor = mainExecutor;
+        shellInit.addInitCallback(this::onInit, this);
     }
 
     public RecentTasks asRecentTasks() {
         return mImpl;
     }
 
-    public void init() {
+    private void onInit() {
         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 7fb961f..1be17f9 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
@@ -32,6 +32,7 @@
 import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS;
 
 import android.app.ActivityManager;
+import android.app.ActivityOptions;
 import android.app.ActivityTaskManager;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
@@ -46,6 +47,7 @@
 import android.util.ArrayMap;
 import android.util.Slog;
 import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
 import android.view.RemoteAnimationAdapter;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
@@ -76,12 +78,14 @@
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.common.split.SplitLayout;
 import com.android.wm.shell.common.split.SplitScreenConstants.SplitPosition;
+import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.draganddrop.DragAndDropPolicy;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 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.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.LegacyTransitions;
 import com.android.wm.shell.transition.Transitions;
 
@@ -139,6 +143,7 @@
     private final DisplayController mDisplayController;
     private final DisplayImeController mDisplayImeController;
     private final DisplayInsetsController mDisplayInsetsController;
+    private final DragAndDropController mDragAndDropController;
     private final Transitions mTransitions;
     private final TransactionPool mTransactionPool;
     private final SplitscreenEventLogger mLogger;
@@ -150,15 +155,21 @@
     // outside the bounds of the roots by being reparented into a higher level fullscreen container
     private SurfaceControl mSplitTasksContainerLayer;
 
-    public SplitScreenController(ShellController shellController,
+    public SplitScreenController(Context context,
+            ShellInit shellInit,
+            ShellController shellController,
             ShellTaskOrganizer shellTaskOrganizer,
-            SyncTransactionQueue syncQueue, Context context,
+            SyncTransactionQueue syncQueue,
             RootTaskDisplayAreaOrganizer rootTDAOrganizer,
-            ShellExecutor mainExecutor, DisplayController displayController,
+            DisplayController displayController,
             DisplayImeController displayImeController,
             DisplayInsetsController displayInsetsController,
-            Transitions transitions, TransactionPool transactionPool, IconProvider iconProvider,
-            Optional<RecentTasksController> recentTasks) {
+            DragAndDropController dragAndDropController,
+            Transitions transitions,
+            TransactionPool transactionPool,
+            IconProvider iconProvider,
+            Optional<RecentTasksController> recentTasks,
+            ShellExecutor mainExecutor) {
         mShellController = shellController;
         mTaskOrganizer = shellTaskOrganizer;
         mSyncQueue = syncQueue;
@@ -168,17 +179,40 @@
         mDisplayController = displayController;
         mDisplayImeController = displayImeController;
         mDisplayInsetsController = displayInsetsController;
+        mDragAndDropController = dragAndDropController;
         mTransitions = transitions;
         mTransactionPool = transactionPool;
         mLogger = new SplitscreenEventLogger();
         mIconProvider = iconProvider;
         mRecentTasksOptional = recentTasks;
+        // 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)) {
+            shellInit.addInitCallback(this::onInit, this);
+        }
     }
 
     public SplitScreen asSplitScreen() {
         return mImpl;
     }
 
+    /**
+     * This will be called after ShellTaskOrganizer has initialized/registered because of the
+     * dependency order.
+     */
+    @VisibleForTesting
+    void onInit() {
+        mShellController.addKeyguardChangeListener(this);
+        if (mStageCoordinator == null) {
+            // TODO: Multi-display
+            mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
+                    mTaskOrganizer, mDisplayController, mDisplayImeController,
+                    mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
+                    mIconProvider, mMainExecutor, mRecentTasksOptional);
+        }
+        mDragAndDropController.setSplitScreenController(this);
+    }
+
     @Override
     public Context getContext() {
         return mContext;
@@ -189,17 +223,6 @@
         return mMainExecutor;
     }
 
-    public void onOrganizerRegistered() {
-        mShellController.addKeyguardChangeListener(this);
-        if (mStageCoordinator == null) {
-            // TODO: Multi-display
-            mStageCoordinator = new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue,
-                    mTaskOrganizer, mDisplayController, mDisplayImeController,
-                    mDisplayInsetsController, mTransitions, mTransactionPool, mLogger,
-                    mIconProvider, mMainExecutor, mRecentTasksOptional);
-        }
-    }
-
     public boolean isSplitScreenVisible() {
         return mStageCoordinator.isSplitScreenVisible();
     }
@@ -334,17 +357,37 @@
 
     public void startShortcut(String packageName, String shortcutId, @SplitPosition int position,
             @Nullable Bundle options, UserHandle user) {
+        IRemoteAnimationRunner wrapper = new IRemoteAnimationRunner.Stub() {
+            @Override
+            public void onAnimationStart(@WindowManager.TransitionOldType int transit,
+                    RemoteAnimationTarget[] apps,
+                    RemoteAnimationTarget[] wallpapers,
+                    RemoteAnimationTarget[] nonApps,
+                    final IRemoteAnimationFinishedCallback finishedCallback) {
+                try {
+                    finishedCallback.onAnimationFinished();
+                } catch (RemoteException e) {
+                    Slog.e(TAG, "Failed to invoke onAnimationFinished", e);
+                }
+                final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+                mStageCoordinator.prepareEvictNonOpeningChildTasks(position, apps, evictWct);
+                mSyncQueue.queue(evictWct);
+            }
+            @Override
+            public void onAnimationCancelled(boolean isKeyguardOccluded) {
+            }
+        };
         options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
                 null /* wct */);
-        final WindowContainerTransaction evictWct = new WindowContainerTransaction();
-        mStageCoordinator.prepareEvictChildTasks(position, evictWct);
+        RemoteAnimationAdapter wrappedAdapter = new RemoteAnimationAdapter(wrapper,
+                0 /* duration */, 0 /* statusBarTransitionDelay */);
+        ActivityOptions activityOptions = ActivityOptions.fromBundle(options);
+        activityOptions.update(ActivityOptions.makeRemoteAnimation(wrappedAdapter));
 
         try {
-            LauncherApps launcherApps =
-                    mContext.getSystemService(LauncherApps.class);
+            LauncherApps launcherApps = mContext.getSystemService(LauncherApps.class);
             launcherApps.startShortcut(packageName, shortcutId, null /* sourceBounds */,
-                    options, user);
-            mSyncQueue.queue(evictWct);
+                    activityOptions.toBundle(), user);
         } catch (ActivityNotFoundException e) {
             Slog.e(TAG, "Failed to launch shortcut", e);
         }
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 66f5bb6..2b3b61b 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
@@ -610,6 +610,15 @@
         }
     }
 
+    void prepareEvictNonOpeningChildTasks(@SplitPosition int position, RemoteAnimationTarget[] apps,
+            WindowContainerTransaction wct) {
+        if (position == mSideStagePosition) {
+            mSideStage.evictNonOpeningChildren(apps, wct);
+        } else {
+            mMainStage.evictNonOpeningChildren(apps, wct);
+        }
+    }
+
     void prepareEvictInvisibleChildTasks(WindowContainerTransaction wct) {
         mMainStage.evictInvisibleChildren(wct);
         mSideStage.evictInvisibleChildren(wct);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
index f6dc68b..f414d69 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java
@@ -20,6 +20,7 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.RemoteAnimationTarget.MODE_OPENING;
 
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES;
 import static com.android.wm.shell.common.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE;
@@ -33,6 +34,7 @@
 import android.graphics.Rect;
 import android.os.IBinder;
 import android.util.SparseArray;
+import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.SurfaceSession;
 import android.window.WindowContainerToken;
@@ -334,6 +336,19 @@
         }
     }
 
+    void evictNonOpeningChildren(RemoteAnimationTarget[] apps, WindowContainerTransaction wct) {
+        final SparseArray<ActivityManager.RunningTaskInfo> toBeEvict = mChildrenTaskInfo.clone();
+        for (int i = 0; i < apps.length; i++) {
+            if (apps[i].mode == MODE_OPENING) {
+                toBeEvict.remove(apps[i].taskId);
+            }
+        }
+        for (int i = toBeEvict.size() - 1; i >= 0; i--) {
+            final ActivityManager.RunningTaskInfo taskInfo = toBeEvict.valueAt(i);
+            wct.reparent(taskInfo.token, null /* parent */, false /* onTop */);
+        }
+    }
+
     void evictInvisibleChildren(WindowContainerTransaction wct) {
         for (int i = mChildrenTaskInfo.size() - 1; i >= 0; i--) {
             final ActivityManager.RunningTaskInfo taskInfo = mChildrenTaskInfo.valueAt(i);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
index fbc9923..379af21 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/startingsurface/StartingWindowController.java
@@ -42,10 +42,12 @@
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.function.TriConsumer;
 import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SingleInstanceRemoteListener;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
 
 /**
  * Implementation to draw the starting window to an application, and remove the starting window
@@ -74,6 +76,7 @@
     private TriConsumer<Integer, Integer, Integer> mTaskLaunchingCallback;
     private final StartingSurfaceImpl mImpl = new StartingSurfaceImpl();
     private final Context mContext;
+    private final ShellTaskOrganizer mShellTaskOrganizer;
     private final ShellExecutor mSplashScreenExecutor;
     /**
      * Need guarded because it has exposed to StartingSurface
@@ -81,14 +84,20 @@
     @GuardedBy("mTaskBackgroundColors")
     private final SparseIntArray mTaskBackgroundColors = new SparseIntArray();
 
-    public StartingWindowController(Context context, ShellExecutor splashScreenExecutor,
-            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm, IconProvider iconProvider,
+    public StartingWindowController(Context context,
+            ShellInit shellInit,
+            ShellTaskOrganizer shellTaskOrganizer,
+            ShellExecutor splashScreenExecutor,
+            StartingWindowTypeAlgorithm startingWindowTypeAlgorithm,
+            IconProvider iconProvider,
             TransactionPool pool) {
         mContext = context;
+        mShellTaskOrganizer = shellTaskOrganizer;
         mStartingSurfaceDrawer = new StartingSurfaceDrawer(context, splashScreenExecutor,
                 iconProvider, pool);
         mStartingWindowTypeAlgorithm = startingWindowTypeAlgorithm;
         mSplashScreenExecutor = splashScreenExecutor;
+        shellInit.addInitCallback(this::onInit, this);
     }
 
     /**
@@ -98,6 +107,10 @@
         return mImpl;
     }
 
+    private void onInit() {
+        mShellTaskOrganizer.initStartingWindow(this);
+    }
+
     @Override
     public Context getContext() {
         return mContext;
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 0427efb..f4fc0c4 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
@@ -45,7 +45,6 @@
     private final Optional<RecentTasksController> mRecentTasks;
     private final ShellTaskOrganizer mShellTaskOrganizer;
     private final KidsModeTaskOrganizer mKidsModeTaskOrganizer;
-    private final ShellExecutor mMainExecutor;
 
     public ShellCommandHandler(
             ShellController shellController,
@@ -64,7 +63,6 @@
         mPipOptional = pipOptional;
         mOneHandedOptional = oneHandedOptional;
         mHideDisplayCutout = hideDisplayCutout;
-        mMainExecutor = mainExecutor;
         // TODO(238217847): To be removed once the command handler dependencies are inverted
         shellController.setShellCommandHandler(this);
     }
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 618028c..f1f317f 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
@@ -44,10 +44,10 @@
 public class ShellController {
     private static final String TAG = ShellController.class.getSimpleName();
 
+    private final ShellInit mShellInit;
     private final ShellExecutor mMainExecutor;
     private final ShellInterfaceImpl mImpl = new ShellInterfaceImpl();
 
-    private ShellInit mShellInit;
     private ShellCommandHandler mShellCommandHandler;
 
     private final CopyOnWriteArrayList<ConfigurationChangeListener> mConfigChangeListeners =
@@ -57,7 +57,8 @@
     private Configuration mLastConfiguration;
 
 
-    public ShellController(ShellExecutor mainExecutor) {
+    public ShellController(ShellInit shellInit, ShellExecutor mainExecutor) {
+        mShellInit = shellInit;
         mMainExecutor = mainExecutor;
     }
 
@@ -69,15 +70,6 @@
     }
 
     /**
-     * Sets the init handler to call back to.
-     * TODO(238217847): This is only exposed this way until we can remove the dependencies from the
-     *                  init handler to other classes.
-     */
-    public void setShellInit(ShellInit shellInit) {
-        mShellInit = shellInit;
-    }
-
-    /**
      * 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.
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 dd7fab7..c250e03 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
@@ -16,7 +16,6 @@
 
 package com.android.wm.shell.sysui;
 
-import static com.android.wm.shell.ShellTaskOrganizer.TASK_LISTENER_TYPE_FULLSCREEN;
 import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_INIT;
 
 import android.os.Build;
@@ -26,149 +25,27 @@
 import androidx.annotation.VisibleForTesting;
 
 import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
-import com.android.wm.shell.bubbles.BubbleController;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.draganddrop.DragAndDropController;
-import com.android.wm.shell.freeform.FreeformComponents;
-import com.android.wm.shell.fullscreen.FullscreenTaskListener;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
-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.startingsurface.StartingWindowController;
-import com.android.wm.shell.transition.DefaultMixedHandler;
-import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.unfold.UnfoldAnimationController;
-import com.android.wm.shell.unfold.UnfoldTransitionHandler;
 
 import java.util.ArrayList;
-import java.util.Optional;
 
 /**
  * The entry point implementation into the shell for initializing shell internal state.  Classes
- * which need to setup on start should inject an instance of this class and add an init callback.
+ * which need to initialize on start of the host SysUI should inject an instance of this class and
+ * add an init callback.
  */
 public class ShellInit {
     private static final String TAG = ShellInit.class.getSimpleName();
 
-    private final DisplayController mDisplayController;
-    private final DisplayImeController mDisplayImeController;
-    private final DisplayInsetsController mDisplayInsetsController;
-    private final DragAndDropController mDragAndDropController;
-    private final ShellTaskOrganizer mShellTaskOrganizer;
-    private final KidsModeTaskOrganizer mKidsModeTaskOrganizer;
-    private final Optional<BubbleController> mBubblesOptional;
-    private final Optional<SplitScreenController> mSplitScreenOptional;
-    private final Optional<PipTouchHandler> mPipTouchHandlerOptional;
-    private final FullscreenTaskListener mFullscreenTaskListener;
-    private final Optional<UnfoldAnimationController> mUnfoldController;
-    private final Optional<UnfoldTransitionHandler> mUnfoldTransitionHandler;
-    private final Optional<FreeformComponents> mFreeformComponentsOptional;
     private final ShellExecutor mMainExecutor;
-    private final Transitions mTransitions;
-    private final StartingWindowController mStartingWindow;
-    private final Optional<RecentTasksController> mRecentTasks;
-    private final Optional<ActivityEmbeddingController> mActivityEmbeddingOptional;
 
     // An ordered list of init callbacks to be made once shell is first started
     private final ArrayList<Pair<String, Runnable>> mInitCallbacks = new ArrayList<>();
     private boolean mHasInitialized;
 
-    public ShellInit(
-            ShellController shellController,
-            DisplayController displayController,
-            DisplayImeController displayImeController,
-            DisplayInsetsController displayInsetsController,
-            DragAndDropController dragAndDropController,
-            ShellTaskOrganizer shellTaskOrganizer,
-            KidsModeTaskOrganizer kidsModeTaskOrganizer,
-            Optional<BubbleController> bubblesOptional,
-            Optional<SplitScreenController> splitScreenOptional,
-            Optional<PipTouchHandler> pipTouchHandlerOptional,
-            FullscreenTaskListener fullscreenTaskListener,
-            Optional<UnfoldAnimationController> unfoldAnimationController,
-            Optional<UnfoldTransitionHandler> unfoldTransitionHandler,
-            Optional<FreeformComponents> freeformComponentsOptional,
-            Optional<RecentTasksController> recentTasks,
-            Optional<ActivityEmbeddingController> activityEmbeddingOptional,
-            Transitions transitions,
-            StartingWindowController startingWindow,
-            ShellExecutor mainExecutor) {
-        mDisplayController = displayController;
-        mDisplayImeController = displayImeController;
-        mDisplayInsetsController = displayInsetsController;
-        mDragAndDropController = dragAndDropController;
-        mShellTaskOrganizer = shellTaskOrganizer;
-        mKidsModeTaskOrganizer = kidsModeTaskOrganizer;
-        mBubblesOptional = bubblesOptional;
-        mSplitScreenOptional = splitScreenOptional;
-        mFullscreenTaskListener = fullscreenTaskListener;
-        mPipTouchHandlerOptional = pipTouchHandlerOptional;
-        mUnfoldController = unfoldAnimationController;
-        mUnfoldTransitionHandler = unfoldTransitionHandler;
-        mFreeformComponentsOptional = freeformComponentsOptional;
-        mRecentTasks = recentTasks;
-        mActivityEmbeddingOptional = activityEmbeddingOptional;
-        mTransitions = transitions;
+
+    public ShellInit(ShellExecutor mainExecutor) {
         mMainExecutor = mainExecutor;
-        mStartingWindow = startingWindow;
-        // TODO(238217847): To be removed once the init dependencies are inverted
-        shellController.setShellInit(this);
-    }
-
-    private void legacyInit() {
-        // Start listening for display and insets changes
-        mDisplayController.initialize();
-        mDisplayInsetsController.initialize();
-        mDisplayImeController.startMonitorDisplays();
-
-        // Setup the shell organizer
-        mShellTaskOrganizer.addListenerForType(
-                mFullscreenTaskListener, TASK_LISTENER_TYPE_FULLSCREEN);
-        mShellTaskOrganizer.initStartingWindow(mStartingWindow);
-        mShellTaskOrganizer.registerOrganizer();
-
-        mSplitScreenOptional.ifPresent(SplitScreenController::onOrganizerRegistered);
-        mBubblesOptional.ifPresent(BubbleController::initialize);
-
-        // Bind the splitscreen impl to the drag drop controller
-        mDragAndDropController.initialize(mSplitScreenOptional);
-
-        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
-            mTransitions.register(mShellTaskOrganizer);
-            mActivityEmbeddingOptional.ifPresent(ActivityEmbeddingController::init);
-            mUnfoldTransitionHandler.ifPresent(UnfoldTransitionHandler::init);
-            mFreeformComponentsOptional.flatMap(f -> f.mTaskTransitionHandler)
-                    .ifPresent(mTransitions::addHandler);
-            if (mSplitScreenOptional.isPresent() && mPipTouchHandlerOptional.isPresent()) {
-                final DefaultMixedHandler mixedHandler = new DefaultMixedHandler(mTransitions,
-                        mPipTouchHandlerOptional.get().getTransitionHandler(),
-                        mSplitScreenOptional.get().getTransitionHandler());
-                // Added at end so that it has highest priority.
-                mTransitions.addHandler(mixedHandler);
-            }
-        }
-
-        // TODO(b/181599115): This should really be the pip controller, but until we can provide the
-        // controller instead of the feature interface, can just initialize the touch handler if
-        // needed
-        mPipTouchHandlerOptional.ifPresent((handler) -> handler.init());
-
-        // Initialize optional freeform
-        mFreeformComponentsOptional.ifPresent(f ->
-                mShellTaskOrganizer.addListenerForType(
-                        f.mTaskListener, ShellTaskOrganizer.TASK_LISTENER_TYPE_FREEFORM));
-
-        mUnfoldController.ifPresent(UnfoldAnimationController::init);
-        mRecentTasks.ifPresent(RecentTasksController::init);
-
-        // Initialize kids mode task organizer
-        mKidsModeTaskOrganizer.initialize(mStartingWindow);
     }
 
     /**
@@ -201,13 +78,9 @@
             final long t1 = SystemClock.uptimeMillis();
             info.second.run();
             final long t2 = SystemClock.uptimeMillis();
-            ProtoLog.v(WM_SHELL_INIT, "\t%s took %dms", info.first, (t2 - t1));
+            ProtoLog.v(WM_SHELL_INIT, "\t%s init took %dms", info.first, (t2 - t1));
         }
         mInitCallbacks.clear();
-
-        // TODO: To be removed
-        legacyInit();
-
         mHasInitialized = true;
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
index 0bec543..afc70a44 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java
@@ -109,6 +109,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -136,6 +137,7 @@
     private final TransactionPool mTransactionPool;
     private final DisplayController mDisplayController;
     private final Context mContext;
+    private final Handler mMainHandler;
     private final ShellExecutor mMainExecutor;
     private final ShellExecutor mAnimExecutor;
     private final TransitionAnimation mTransitionAnimation;
@@ -167,27 +169,33 @@
         }
     };
 
-    DefaultTransitionHandler(@NonNull DisplayController displayController,
-            @NonNull TransactionPool transactionPool, Context context,
+    DefaultTransitionHandler(@NonNull Context context,
+            @NonNull ShellInit shellInit,
+            @NonNull DisplayController displayController,
+            @NonNull TransactionPool transactionPool,
             @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler,
             @NonNull ShellExecutor animExecutor) {
         mDisplayController = displayController;
         mTransactionPool = transactionPool;
         mContext = context;
+        mMainHandler = mainHandler;
         mMainExecutor = mainExecutor;
         mAnimExecutor = animExecutor;
         mTransitionAnimation = new TransitionAnimation(context, false /* debug */, Transitions.TAG);
         mCurrentUserId = UserHandle.myUserId();
+        mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class);
+        shellInit.addInitCallback(this::onInit, this);
+    }
 
-        mDevicePolicyManager = context.getSystemService(DevicePolicyManager.class);
+    private void onInit() {
         updateEnterpriseThumbnailDrawable();
         mContext.registerReceiver(
                 mEnterpriseResourceUpdatedReceiver,
                 new IntentFilter(ACTION_DEVICE_POLICY_RESOURCE_UPDATED),
                 /* broadcastPermission = */ null,
-                mainHandler);
+                mMainHandler);
 
-        AttributeCache.init(context);
+        AttributeCache.init(mContext);
     }
 
     private void updateEnterpriseThumbnailDrawable() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SplitscreenPipMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SplitscreenPipMixedHandler.java
new file mode 100644
index 0000000..678e91f
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/SplitscreenPipMixedHandler.java
@@ -0,0 +1,55 @@
+/*
+ * 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.transition;
+
+import com.android.wm.shell.pip.phone.PipTouchHandler;
+import com.android.wm.shell.splitscreen.SplitScreenController;
+import com.android.wm.shell.sysui.ShellInit;
+
+import java.util.Optional;
+
+/**
+ * Handles transitions between the Splitscreen and PIP components.
+ */
+public class SplitscreenPipMixedHandler {
+
+    private final Optional<SplitScreenController> mSplitScreenOptional;
+    private final Optional<PipTouchHandler> mPipTouchHandlerOptional;
+    private final Transitions mTransitions;
+
+    public SplitscreenPipMixedHandler(ShellInit shellInit,
+            Optional<SplitScreenController> splitScreenControllerOptional,
+            Optional<PipTouchHandler> pipTouchHandlerOptional,
+            Transitions transitions) {
+        mSplitScreenOptional = splitScreenControllerOptional;
+        mPipTouchHandlerOptional = pipTouchHandlerOptional;
+        mTransitions = transitions;
+        if (Transitions.ENABLE_SHELL_TRANSITIONS
+                && mSplitScreenOptional.isPresent() && mPipTouchHandlerOptional.isPresent()) {
+            shellInit.addInitCallback(this::onInit, this);
+        }
+    }
+
+    private void onInit() {
+        // Special handling for initializing based on multiple components
+        final DefaultMixedHandler mixedHandler = new DefaultMixedHandler(mTransitions,
+                mPipTouchHandlerOptional.get().getTransitionHandler(),
+                mSplitScreenOptional.get().getTransitionHandler());
+        // Added at end so that it has highest priority.
+        mTransitions.addHandler(mixedHandler);
+    }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
index 881b7a1..e6006c4 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java
@@ -59,13 +59,13 @@
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.common.annotations.ExternalThread;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -111,6 +111,7 @@
     private final ShellExecutor mMainExecutor;
     private final ShellExecutor mAnimExecutor;
     private final TransitionPlayerImpl mPlayerImpl;
+    private final DefaultTransitionHandler mDefaultTransitionHandler;
     private final RemoteTransitionHandler mRemoteTransitionHandler;
     private final DisplayController mDisplayController;
     private final ShellTransitionImpl mImpl = new ShellTransitionImpl();
@@ -136,8 +137,11 @@
     /** Keeps track of currently playing transitions in the order of receipt. */
     private final ArrayList<ActiveTransition> mActiveTransitions = new ArrayList<>();
 
-    public Transitions(@NonNull WindowOrganizer organizer, @NonNull TransactionPool pool,
-            @NonNull DisplayController displayController, @NonNull Context context,
+    public Transitions(@NonNull Context context,
+            @NonNull ShellInit shellInit,
+            @NonNull WindowOrganizer organizer,
+            @NonNull TransactionPool pool,
+            @NonNull DisplayController displayController,
             @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler,
             @NonNull ShellExecutor animExecutor) {
         mOrganizer = organizer;
@@ -146,33 +150,35 @@
         mAnimExecutor = animExecutor;
         mDisplayController = displayController;
         mPlayerImpl = new TransitionPlayerImpl();
+        mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit,
+                displayController, pool, mainExecutor, mainHandler, animExecutor);
+        mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor);
+        shellInit.addInitCallback(this::onInit, this);
+    }
+
+    private void onInit() {
         // The very last handler (0 in the list) should be the default one.
-        mHandlers.add(new DefaultTransitionHandler(displayController, pool, context, mainExecutor,
-                mainHandler, animExecutor));
+        mHandlers.add(mDefaultTransitionHandler);
         // Next lowest priority is remote transitions.
-        mRemoteTransitionHandler = new RemoteTransitionHandler(mainExecutor);
         mHandlers.add(mRemoteTransitionHandler);
 
-        ContentResolver resolver = context.getContentResolver();
+        ContentResolver resolver = mContext.getContentResolver();
         mTransitionAnimationScaleSetting = Settings.Global.getFloat(resolver,
                 Settings.Global.TRANSITION_ANIMATION_SCALE,
-                context.getResources().getFloat(
+                mContext.getResources().getFloat(
                         R.dimen.config_appTransitionAnimationDurationScaleDefault));
         dispatchAnimScaleSetting(mTransitionAnimationScaleSetting);
 
         resolver.registerContentObserver(
                 Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), false,
                 new SettingsObserver());
-    }
 
-    private Transitions() {
-        mOrganizer = null;
-        mContext = null;
-        mMainExecutor = null;
-        mAnimExecutor = null;
-        mDisplayController = null;
-        mPlayerImpl = null;
-        mRemoteTransitionHandler = null;
+        if (Transitions.ENABLE_SHELL_TRANSITIONS) {
+            // Register this transition handler with Core
+            mOrganizer.registerTransitionPlayer(mPlayerImpl);
+            // Pre-load the instance.
+            TransitionMetrics.getInstance();
+        }
     }
 
     public ShellTransitions asRemoteTransitions() {
@@ -195,14 +201,6 @@
         }
     }
 
-    /** Register this transition handler with Core */
-    public void register(ShellTaskOrganizer taskOrganizer) {
-        if (mPlayerImpl == null) return;
-        taskOrganizer.registerTransitionPlayer(mPlayerImpl);
-        // Pre-load the instance.
-        TransitionMetrics.getInstance();
-    }
-
     /**
      * Adds a handler candidate.
      * @see TransitionHandler
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
index 05a024a..6b59e31 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldAnimationController.java
@@ -24,13 +24,14 @@
 import android.util.SparseArray;
 import android.view.SurfaceControl;
 
+import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.unfold.ShellUnfoldProgressProvider.UnfoldListener;
 import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
 
 import java.util.List;
 import java.util.Optional;
-import java.util.concurrent.Executor;
 
 import dagger.Lazy;
 
@@ -47,7 +48,7 @@
 public class UnfoldAnimationController implements UnfoldListener {
 
     private final ShellUnfoldProgressProvider mUnfoldProgressProvider;
-    private final Executor mExecutor;
+    private final ShellExecutor mExecutor;
     private final TransactionPool mTransactionPool;
     private final List<UnfoldTaskAnimator> mAnimators;
     private final Lazy<Optional<UnfoldTransitionHandler>> mUnfoldTransitionHandler;
@@ -55,28 +56,36 @@
     private final SparseArray<SurfaceControl> mTaskSurfaces = new SparseArray<>();
     private final SparseArray<UnfoldTaskAnimator> mAnimatorsByTaskId = new SparseArray<>();
 
-    public UnfoldAnimationController(@NonNull TransactionPool transactionPool,
+    public UnfoldAnimationController(
+            @NonNull ShellInit shellInit,
+            @NonNull TransactionPool transactionPool,
             @NonNull ShellUnfoldProgressProvider unfoldProgressProvider,
             @NonNull List<UnfoldTaskAnimator> animators,
             @NonNull Lazy<Optional<UnfoldTransitionHandler>> unfoldTransitionHandler,
-            @NonNull Executor executor) {
+            @NonNull ShellExecutor executor) {
         mUnfoldProgressProvider = unfoldProgressProvider;
         mUnfoldTransitionHandler = unfoldTransitionHandler;
         mTransactionPool = transactionPool;
         mExecutor = executor;
         mAnimators = animators;
+        // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
+        //                    override for this controller from the base module
+        if (unfoldProgressProvider != ShellUnfoldProgressProvider.NO_PROVIDER) {
+            shellInit.addInitCallback(this::onInit, this);
+        }
     }
 
     /**
      * Initializes the controller, starts listening for the external events
      */
-    public void init() {
+    public void onInit() {
         mUnfoldProgressProvider.addListener(mExecutor, this);
 
         for (int i = 0; i < mAnimators.size(); i++) {
             final UnfoldTaskAnimator animator = mAnimators.get(i);
             animator.init();
-            animator.start();
+            // TODO(b/238217847): See #provideSplitTaskUnfoldAnimatorBase
+            mExecutor.executeDelayed(animator::start, 0);
         }
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
index 9bf32fa..5d7b629 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java
@@ -28,6 +28,7 @@
 import androidx.annotation.Nullable;
 
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 import com.android.wm.shell.transition.Transitions.TransitionFinishCallback;
 import com.android.wm.shell.transition.Transitions.TransitionHandler;
@@ -59,11 +60,13 @@
 
     private final List<UnfoldTaskAnimator> mAnimators = new ArrayList<>();
 
-    public UnfoldTransitionHandler(ShellUnfoldProgressProvider unfoldProgressProvider,
+    public UnfoldTransitionHandler(ShellInit shellInit,
+            ShellUnfoldProgressProvider unfoldProgressProvider,
             FullscreenUnfoldTaskAnimator fullscreenUnfoldAnimator,
             SplitTaskUnfoldAnimator splitUnfoldTaskAnimator,
             TransactionPool transactionPool,
-            Executor executor, Transitions transitions) {
+            Executor executor,
+            Transitions transitions) {
         mUnfoldProgressProvider = unfoldProgressProvider;
         mTransactionPool = transactionPool;
         mExecutor = executor;
@@ -71,9 +74,18 @@
 
         mAnimators.add(splitUnfoldTaskAnimator);
         mAnimators.add(fullscreenUnfoldAnimator);
+        // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic
+        //                    override for this controller from the base module
+        if (unfoldProgressProvider != ShellUnfoldProgressProvider.NO_PROVIDER
+                && Transitions.ENABLE_SHELL_TRANSITIONS) {
+            shellInit.addInitCallback(this::onInit, this);
+        }
     }
 
-    public void init() {
+    /**
+     * Called when the transition handler is initialized.
+     */
+    public void onInit() {
         for (int i = 0; i < mAnimators.size(); i++) {
             mAnimators.get(i).init();
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java
index 6cd5677..4bcdcaa 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellInitTest.java
@@ -24,25 +24,8 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
-import com.android.wm.shell.bubbles.BubbleController;
-import com.android.wm.shell.common.DisplayController;
-import com.android.wm.shell.common.DisplayImeController;
-import com.android.wm.shell.common.DisplayInsetsController;
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.draganddrop.DragAndDropController;
-import com.android.wm.shell.freeform.FreeformComponents;
-import com.android.wm.shell.fullscreen.FullscreenTaskListener;
-import com.android.wm.shell.kidsmode.KidsModeTaskOrganizer;
-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.startingsurface.StartingWindowController;
-import com.android.wm.shell.sysui.ShellController;
 import com.android.wm.shell.sysui.ShellInit;
-import com.android.wm.shell.transition.Transitions;
-import com.android.wm.shell.unfold.UnfoldAnimationController;
-import com.android.wm.shell.unfold.UnfoldTransitionHandler;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -51,31 +34,12 @@
 import org.mockito.MockitoAnnotations;
 
 import java.util.ArrayList;
-import java.util.Optional;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class ShellInitTest extends ShellTestCase {
 
-    @Mock private ShellController mShellController;
-    @Mock private DisplayController mDisplayController;
-    @Mock private DisplayImeController mDisplayImeController;
-    @Mock private DisplayInsetsController mDisplayInsetsController;
-    @Mock private DragAndDropController mDragAndDropController;
-    @Mock private ShellTaskOrganizer mShellTaskOrganizer;
-    @Mock private KidsModeTaskOrganizer mKidsModeTaskOrganizer;
-    @Mock private Optional<BubbleController> mBubblesOptional;
-    @Mock private Optional<SplitScreenController> mSplitScreenOptional;
-    @Mock private Optional<PipTouchHandler> mPipTouchHandlerOptional;
-    @Mock private FullscreenTaskListener mFullscreenTaskListener;
-    @Mock private Optional<UnfoldAnimationController> mUnfoldAnimationController;
-    @Mock private Optional<UnfoldTransitionHandler> mUnfoldTransitionHandler;
-    @Mock private Optional<FreeformComponents> mFreeformComponentsOptional;
-    @Mock private Optional<RecentTasksController> mRecentTasks;
-    @Mock private Optional<ActivityEmbeddingController> mActivityEmbeddingController;
-    @Mock private Transitions mTransitions;
-    @Mock private StartingWindowController mStartingWindow;
     @Mock private ShellExecutor mMainExecutor;
 
     private ShellInit mImpl;
@@ -83,12 +47,7 @@
     @Before
     public void setUp() {
         MockitoAnnotations.initMocks(this);
-        mImpl = new ShellInit(mShellController, mDisplayController, mDisplayImeController,
-                mDisplayInsetsController, mDragAndDropController, mShellTaskOrganizer,
-                mKidsModeTaskOrganizer, mBubblesOptional, mSplitScreenOptional,
-                mPipTouchHandlerOptional, mFullscreenTaskListener, mUnfoldAnimationController,
-                mUnfoldTransitionHandler, mFreeformComponentsOptional, mRecentTasks,
-                mActivityEmbeddingController, mTransitions, mStartingWindow, mMainExecutor);
+        mImpl = new ShellInit(mMainExecutor);
     }
 
     @Test
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 3dd0032..7517e8a 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
@@ -36,11 +36,11 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
 import android.app.ActivityManager.RunningTaskInfo;
 import android.app.TaskInfo;
-import android.content.Context;
 import android.content.LocusId;
 import android.content.pm.ParceledListSlice;
 import android.os.Binder;
@@ -58,9 +58,8 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.wm.shell.common.ShellExecutor;
-import com.android.wm.shell.common.SyncTransactionQueue;
-import com.android.wm.shell.common.TransactionPool;
 import com.android.wm.shell.compatui.CompatUIController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -84,13 +83,11 @@
     @Mock
     private ITaskOrganizerController mTaskOrganizerController;
     @Mock
-    private Context mContext;
-    @Mock
     private CompatUIController mCompatUI;
+    @Mock
+    private ShellInit mShellInit;
 
     ShellTaskOrganizer mOrganizer;
-    private final SyncTransactionQueue mSyncTransactionQueue = mock(SyncTransactionQueue.class);
-    private final TransactionPool mTransactionPool = mock(TransactionPool.class);
     private final ShellExecutor mTestExecutor = mock(ShellExecutor.class);
 
     private class TrackingTaskListener implements ShellTaskOrganizer.TaskListener {
@@ -135,8 +132,13 @@
             doReturn(ParceledListSlice.<TaskAppearedInfo>emptyList())
                     .when(mTaskOrganizerController).registerTaskOrganizer(any());
         } catch (RemoteException e) {}
-        mOrganizer = spy(new ShellTaskOrganizer(mTaskOrganizerController, mTestExecutor, mContext,
-                mCompatUI, Optional.empty(), Optional.empty()));
+        mOrganizer = spy(new ShellTaskOrganizer(mShellInit, mTaskOrganizerController,
+                mCompatUI, Optional.empty(), Optional.empty(), mTestExecutor));
+    }
+
+    @Test
+    public void instantiate_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
     }
 
     @Test
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
new file mode 100644
index 0000000..bfe3b54
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java
@@ -0,0 +1,67 @@
+/*
+ * 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.activityembedding;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+
+import static org.junit.Assume.assumeTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
+import com.android.wm.shell.transition.Transitions;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the activity embedding controller.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:ActivityEmbeddingControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ActivityEmbeddingControllerTests extends ShellTestCase {
+
+    private @Mock Context mContext;
+    private @Mock ShellInit mShellInit;
+    private @Mock Transitions mTransitions;
+    private ActivityEmbeddingController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mController = spy(new ActivityEmbeddingController(mContext, mShellInit, mTransitions));
+    }
+
+    @Test
+    public void instantiate_addInitCallback() {
+        assumeTrue(Transitions.ENABLE_SHELL_TRANSITIONS);
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java
new file mode 100644
index 0000000..b8aa8e7
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayChangeControllerTests.java
@@ -0,0 +1,64 @@
+/*
+ * 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.common;
+
+import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.view.IWindowManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the display change controller.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:DisplayChangeControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DisplayChangeControllerTests extends ShellTestCase {
+
+    private @Mock IWindowManager mWM;
+    private @Mock ShellInit mShellInit;
+    private @Mock ShellExecutor mMainExecutor;
+    private DisplayChangeController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mController = spy(new DisplayChangeController(mWM, mShellInit, mMainExecutor));
+    }
+
+    @Test
+    public void instantiate_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
new file mode 100644
index 0000000..1e5e153
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java
@@ -0,0 +1,65 @@
+/*
+ * 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.common;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.view.IWindowManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the display controller.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:DisplayControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class DisplayControllerTests extends ShellTestCase {
+
+    private @Mock Context mContext;
+    private @Mock IWindowManager mWM;
+    private @Mock ShellInit mShellInit;
+    private @Mock ShellExecutor mMainExecutor;
+    private DisplayController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        mController = new DisplayController(mContext, mWM, mShellInit, mMainExecutor);
+    }
+
+    @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), eq(mController));
+    }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
index 5b691f2..9967e5f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java
@@ -26,6 +26,7 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.verifyZeroInteractions;
 
@@ -40,26 +41,32 @@
 
 import com.android.internal.view.IInputMethodManager;
 import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
 
 import java.util.concurrent.Executor;
 
 @SmallTest
 public class DisplayImeControllerTest extends ShellTestCase {
 
+    @Mock
     private SurfaceControl.Transaction mT;
-    private DisplayImeController.PerDisplay mPerDisplay;
+    @Mock
     private IInputMethodManager mMock;
+    @Mock
+    private ShellInit mShellInit;
+    private DisplayImeController.PerDisplay mPerDisplay;
     private Executor mExecutor;
 
     @Before
     public void setUp() throws Exception {
-        mT = mock(SurfaceControl.Transaction.class);
-        mMock = mock(IInputMethodManager.class);
+        MockitoAnnotations.initMocks(this);
         mExecutor = spy(Runnable::run);
-        mPerDisplay = new DisplayImeController(null, null, null, mExecutor, new TransactionPool() {
+        mPerDisplay = new DisplayImeController(null, mShellInit, null, null, new TransactionPool() {
             @Override
             public SurfaceControl.Transaction acquire() {
                 return mT;
@@ -68,7 +75,7 @@
             @Override
             public void release(SurfaceControl.Transaction t) {
             }
-        }) {
+        }, mExecutor) {
             @Override
             public IInputMethodManager getImms() {
                 return mMock;
@@ -79,6 +86,11 @@
     }
 
     @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
     public void insetsControlChanged_schedulesNoWorkOnExecutor() {
         mPerDisplay.insetsControlChanged(insetsStateWithIme(false), insetsSourceControl());
         verifyZeroInteractions(mExecutor);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
index 4a7fd3d..5f5a3c5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayInsetsControllerTest.java
@@ -19,6 +19,7 @@
 import static android.view.Display.DEFAULT_DISPLAY;
 
 import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.notNull;
 import static org.mockito.Mockito.times;
@@ -37,6 +38,7 @@
 
 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.Test;
@@ -55,6 +57,8 @@
     private IWindowManager mWm;
     @Mock
     private DisplayController mDisplayController;
+    @Mock
+    private ShellInit mShellInit;
     private DisplayInsetsController mController;
     private SparseArray<IDisplayWindowInsetsController> mInsetsControllersByDisplayId;
     private TestShellExecutor mExecutor;
@@ -69,11 +73,16 @@
         mInsetsControllersByDisplayId = new SparseArray<>();
         mDisplayIdCaptor =  ArgumentCaptor.forClass(Integer.class);
         mInsetsControllerCaptor = ArgumentCaptor.forClass(IDisplayWindowInsetsController.class);
-        mController = new DisplayInsetsController(mWm, mDisplayController, mExecutor);
+        mController = new DisplayInsetsController(mWm, mShellInit, mDisplayController, mExecutor);
         addDisplay(DEFAULT_DISPLAY);
     }
 
     @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
     public void testOnDisplayAdded_setsDisplayWindowInsetsControllerOnWMService()
             throws RemoteException {
         addDisplay(SECOND_DISPLAY);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
index e20997199..b6dbcf2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java
@@ -50,6 +50,7 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -57,8 +58,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.Optional;
-
 /**
  * Tests for the drag and drop controller.
  */
@@ -69,6 +68,8 @@
     @Mock
     private Context mContext;
     @Mock
+    private ShellInit mShellInit;
+    @Mock
     private ShellController mShellController;
     @Mock
     private DisplayController mDisplayController;
@@ -88,9 +89,14 @@
     @Before
     public void setUp() throws RemoteException {
         MockitoAnnotations.initMocks(this);
-        mController = new DragAndDropController(mContext, mShellController, mDisplayController,
-                mUiEventLogger, mIconProvider, mMainExecutor);
-        mController.initialize(Optional.of(mSplitScreenController));
+        mController = new DragAndDropController(mContext, mShellInit, mShellController,
+                mDisplayController, mUiEventLogger, mIconProvider, mMainExecutor);
+        mController.onInit();
+    }
+
+    @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(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 184a8df..a919ad0 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,7 +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.startingsurface.StartingWindowController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -73,7 +73,7 @@
     @Mock private WindowContainerToken mToken;
     @Mock private WindowContainerTransaction mTransaction;
     @Mock private KidsModeSettingsObserver mObserver;
-    @Mock private StartingWindowController mStartingWindowController;
+    @Mock private ShellInit mShellInit;
     @Mock private DisplayInsetsController mDisplayInsetsController;
 
     KidsModeTaskOrganizer mOrganizer;
@@ -87,10 +87,9 @@
         } catch (RemoteException e) {
         }
         // NOTE: KidsModeTaskOrganizer should have a null CompatUIController.
-        mOrganizer = spy(new KidsModeTaskOrganizer(mTaskOrganizerController, mTestExecutor,
-                mHandler, mContext, mSyncTransactionQueue, mDisplayController,
-                mDisplayInsetsController, Optional.empty(), Optional.empty(), mObserver));
-        mOrganizer.initialize(mStartingWindowController);
+        mOrganizer = spy(new KidsModeTaskOrganizer(mContext, mTaskOrganizerController,
+                mSyncTransactionQueue, mDisplayController, mDisplayInsetsController,
+                Optional.empty(), Optional.empty(), mObserver, mTestExecutor, mHandler));
         doReturn(mTransaction).when(mOrganizer).getWindowContainerTransaction();
         doReturn(new InsetsState()).when(mDisplayController).getInsetsState(DEFAULT_DISPLAY);
     }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
index 74519ea..ecefd89 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java
@@ -38,6 +38,7 @@
 import com.android.wm.shell.pip.PipTaskOrganizer;
 import com.android.wm.shell.pip.PipTransitionController;
 import com.android.wm.shell.pip.PipUiEventLogger;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -78,6 +79,9 @@
     private PipUiEventLogger mPipUiEventLogger;
 
     @Mock
+    private ShellInit mShellInit;
+
+    @Mock
     private ShellExecutor mMainExecutor;
 
     private PipBoundsState mPipBoundsState;
@@ -104,11 +108,11 @@
         PipMotionHelper pipMotionHelper = new PipMotionHelper(mContext, mPipBoundsState,
                 mPipTaskOrganizer, mPhonePipMenuController, mPipSnapAlgorithm,
                 mMockPipTransitionController, mFloatingContentCoordinator);
-        mPipTouchHandler = new PipTouchHandler(mContext, mPhonePipMenuController,
-                mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer,
-                pipMotionHelper, mFloatingContentCoordinator, mPipUiEventLogger,
-                mMainExecutor);
-        mPipTouchHandler.init();
+        mPipTouchHandler = new PipTouchHandler(mContext, mShellInit, mPhonePipMenuController,
+                mPipBoundsAlgorithm, mPipBoundsState, mPipTaskOrganizer, pipMotionHelper,
+                mFloatingContentCoordinator, mPipUiEventLogger, mMainExecutor);
+        // We aren't actually using ShellInit, so just call init directly
+        mPipTouchHandler.onInit();
         mMotionHelper = Mockito.spy(mPipTouchHandler.getMotionHelper());
         mPipResizeGestureHandler = Mockito.spy(mPipTouchHandler.getPipResizeGestureHandler());
         mPipTouchHandler.setPipMotionHelper(mMotionHelper);
@@ -133,6 +137,11 @@
     }
 
     @Test
+    public void instantiate_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
     public void updateMovementBounds_minMaxBounds() {
         final int shorterLength = Math.min(mPipBoundsState.getDisplayBounds().width(),
                 mPipBoundsState.getDisplayBounds().height());
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 b1e0911..d406a4e 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
@@ -48,6 +48,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.ShellInit;
 import com.android.wm.shell.util.GroupedRecentTaskInfo;
 import com.android.wm.shell.util.SplitBounds;
 
@@ -71,6 +72,8 @@
     private Context mContext;
     @Mock
     private TaskStackListenerImpl mTaskStackListener;
+    @Mock
+    private ShellInit mShellInit;
 
     private ShellTaskOrganizer mShellTaskOrganizer;
     private RecentTasksController mRecentTasksController;
@@ -80,10 +83,11 @@
     public void setUp() {
         mMainExecutor = new TestShellExecutor();
         when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class));
-        mRecentTasksController = spy(new RecentTasksController(mContext, mTaskStackListener,
-                mMainExecutor));
-        mShellTaskOrganizer = new ShellTaskOrganizer(mMainExecutor, mContext,
-                null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController));
+        mRecentTasksController = spy(new RecentTasksController(mContext, mShellInit,
+                mTaskStackListener, mMainExecutor));
+        mShellTaskOrganizer = new ShellTaskOrganizer(mShellInit,
+                null /* sizeCompatUI */, Optional.empty(), Optional.of(mRecentTasksController),
+                mMainExecutor);
     }
 
     @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 c7a261f..10788f9 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
@@ -51,8 +51,10 @@
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 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.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.transition.Transitions;
 
 import org.junit.Before;
@@ -71,6 +73,7 @@
 public class SplitScreenControllerTests extends ShellTestCase {
 
     @Mock ShellController mShellController;
+    @Mock ShellInit mShellInit;
     @Mock ShellTaskOrganizer mTaskOrganizer;
     @Mock SyncTransactionQueue mSyncQueue;
     @Mock RootTaskDisplayAreaOrganizer mRootTDAOrganizer;
@@ -78,6 +81,7 @@
     @Mock DisplayController mDisplayController;
     @Mock DisplayImeController mDisplayImeController;
     @Mock DisplayInsetsController mDisplayInsetsController;
+    @Mock DragAndDropController mDragAndDropController;
     @Mock Transitions mTransitions;
     @Mock TransactionPool mTransactionPool;
     @Mock IconProvider mIconProvider;
@@ -88,16 +92,21 @@
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
-        mSplitScreenController = spy(new SplitScreenController(mShellController, mTaskOrganizer,
-                mSyncQueue, mContext, mRootTDAOrganizer, mMainExecutor, mDisplayController,
-                mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool,
-                mIconProvider, mRecentTasks));
+        mSplitScreenController = spy(new SplitScreenController(mContext, mShellInit,
+                mShellController, mTaskOrganizer, mSyncQueue, mRootTDAOrganizer, mDisplayController,
+                mDisplayImeController, mDisplayInsetsController, mDragAndDropController,
+                mTransitions, mTransactionPool, mIconProvider, mRecentTasks, mMainExecutor));
+    }
+
+    @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
     }
 
     @Test
     public void testControllerRegistersKeyguardChangeListener() {
         when(mDisplayController.getDisplayLayout(anyInt())).thenReturn(new DisplayLayout());
-        mSplitScreenController.onOrganizerRegistered();
+        mSplitScreenController.onInit();
         verify(mShellController, times(1)).addKeyguardChangeListener(any());
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
new file mode 100644
index 0000000..35515e3
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/startingsurface/StartingWindowControllerTests.java
@@ -0,0 +1,80 @@
+/*
+ * 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.startingsurface;
+
+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.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.view.Display;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.launcher3.icons.IconProvider;
+import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.ShellTestCase;
+import com.android.wm.shell.common.ShellExecutor;
+import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Tests for the starting window controller.
+ *
+ * Build/Install/Run:
+ *  atest WMShellUnitTests:StartingWindowControllerTests
+ */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class StartingWindowControllerTests extends ShellTestCase {
+
+    private @Mock Context mContext;
+    private @Mock DisplayManager mDisplayManager;
+    private @Mock ShellInit mShellInit;
+    private @Mock ShellTaskOrganizer mTaskOrganizer;
+    private @Mock ShellExecutor mMainExecutor;
+    private @Mock StartingWindowTypeAlgorithm mTypeAlgorithm;
+    private @Mock IconProvider mIconProvider;
+    private @Mock TransactionPool mTransactionPool;
+    private StartingWindowController mController;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mock(Display.class)).when(mDisplayManager).getDisplay(anyInt());
+        doReturn(mDisplayManager).when(mContext).getSystemService(eq(DisplayManager.class));
+        mController = new StartingWindowController(mContext, mShellInit, mTaskOrganizer,
+                mMainExecutor, mTypeAlgorithm, mIconProvider, mTransactionPool);
+    }
+
+    @Test
+    public void instantiate_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), 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 1c0e46f..02311ba 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
@@ -43,6 +43,8 @@
 public class ShellControllerTest extends ShellTestCase {
 
     @Mock
+    private ShellInit mShellInit;
+    @Mock
     private ShellExecutor mExecutor;
 
     private ShellController mController;
@@ -54,7 +56,7 @@
         MockitoAnnotations.initMocks(this);
         mKeyguardChangeListener = new TestKeyguardChangeListener();
         mConfigChangeListener = new TestConfigurationChangeListener();
-        mController = new ShellController(mExecutor);
+        mController = new ShellController(mShellInit, mExecutor);
         mController.onConfigurationChanged(getConfigurationCopy());
     }
 
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
index e2f2b71..388792b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java
@@ -84,6 +84,7 @@
 import com.android.wm.shell.common.DisplayController;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -117,6 +118,14 @@
     }
 
     @Test
+    public void instantiate_addInitCallback() {
+        ShellInit shellInit = mock(ShellInit.class);
+        final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool,
+                createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor);
+        verify(shellInit, times(1)).addInitCallback(any(), eq(t));
+    }
+
+    @Test
     public void testBasicTransitionFlow() {
         Transitions transitions = createTestTransitions();
         transitions.replaceDefaultHandlerForTest(mDefaultHandler);
@@ -832,14 +841,18 @@
         } catch (RemoteException e) {
             // No remote stuff happening, so this can't be hit
         }
-        DisplayController out = new DisplayController(mContext, mockWM, mMainExecutor);
-        out.initialize();
+        ShellInit shellInit = new ShellInit(mMainExecutor);
+        DisplayController out = new DisplayController(mContext, mockWM, shellInit, mMainExecutor);
+        shellInit.init();
         return out;
     }
 
     private Transitions createTestTransitions() {
-        return new Transitions(mOrganizer, mTransactionPool, createTestDisplayController(),
-                mContext, mMainExecutor, mMainHandler, mAnimExecutor);
+        ShellInit shellInit = new ShellInit(mMainExecutor);
+        final Transitions t = new Transitions(mContext, shellInit, mOrganizer, mTransactionPool,
+                createTestDisplayController(), mMainExecutor, mMainHandler, mAnimExecutor);
+        shellInit.init();
+        return t;
     }
 //
 //    private class TestDisplayController extends DisplayController {
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java
index 46de607..81eefe2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldAnimationControllerTest.java
@@ -20,7 +20,10 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager.RunningTaskInfo;
@@ -33,6 +36,7 @@
 import com.android.wm.shell.TestRunningTaskInfoBuilder;
 import com.android.wm.shell.TestShellExecutor;
 import com.android.wm.shell.common.TransactionPool;
+import com.android.wm.shell.sysui.ShellInit;
 import com.android.wm.shell.unfold.animation.UnfoldTaskAnimator;
 
 import org.junit.Before;
@@ -65,6 +69,8 @@
     @Mock
     private UnfoldTransitionHandler mUnfoldTransitionHandler;
     @Mock
+    private ShellInit mShellInit;
+    @Mock
     private SurfaceControl mLeash;
 
     private UnfoldAnimationController mUnfoldAnimationController;
@@ -85,6 +91,7 @@
         animators.add(mTaskAnimator1);
         animators.add(mTaskAnimator2);
         mUnfoldAnimationController = new UnfoldAnimationController(
+                mShellInit,
                 mTransactionPool,
                 mProgressProvider,
                 animators,
@@ -94,6 +101,11 @@
     }
 
     @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
     public void testAppearedMatchingTask_appliesUnfoldProgress() {
         mTaskAnimator1.setTaskMatcher((info) -> info.getWindowingMode() == 2);
         RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder()
@@ -244,7 +256,8 @@
 
     @Test
     public void testInit_initsAndStartsAnimators() {
-        mUnfoldAnimationController.init();
+        mUnfoldAnimationController.onInit();
+        mShellExecutor.flushAll();
 
         assertThat(mTaskAnimator1.mInitialized).isTrue();
         assertThat(mTaskAnimator1.mStarted).isTrue();
diff --git a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
index 5ce08b7..2742558 100644
--- a/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
+++ b/packages/SettingsLib/ActivityEmbedding/AndroidManifest.xml
@@ -16,7 +16,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.settingslib.activityembedding">
+          package="com.android.settingslib.widget">
 
     <uses-sdk android:minSdkVersion="21" />
 
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml
index 1c47f5f..244b367 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/AndroidManifest.xml
@@ -16,7 +16,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.settingslib.collapsingtoolbar">
+          package="com.android.settingslib.widget">
 
     <uses-sdk android:minSdkVersion="29" />
 
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/BasePreferencesFragment.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/BasePreferencesFragment.java
index 8ebbac3..3582897 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/BasePreferencesFragment.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/BasePreferencesFragment.java
@@ -20,6 +20,7 @@
 import androidx.preference.PreferenceFragmentCompat;
 
 import com.android.settingslib.utils.BuildCompatUtils;
+import com.android.settingslib.widget.R;
 
 import com.google.android.material.appbar.AppBarLayout;
 
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
index 77d6583..8c8b478 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarBaseActivity.java
@@ -27,6 +27,7 @@
 import androidx.fragment.app.FragmentActivity;
 
 import com.android.settingslib.utils.BuildCompatUtils;
+import com.android.settingslib.widget.R;
 
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.appbar.CollapsingToolbarLayout;
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
index 31e8cc7..ec091bf 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/CollapsingToolbarDelegate.java
@@ -30,6 +30,8 @@
 import androidx.annotation.Nullable;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
 
+import com.android.settingslib.widget.R;
+
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.appbar.CollapsingToolbarLayout;
 
diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
index 1ead2f3..a8c7a3f 100644
--- a/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
+++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/src/com/android/settingslib/collapsingtoolbar/widget/CollapsingCoordinatorLayout.java
@@ -36,7 +36,7 @@
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.coordinatorlayout.widget.CoordinatorLayout;
 
-import com.android.settingslib.collapsingtoolbar.R;
+import com.android.settingslib.widget.R;
 
 import com.google.android.material.appbar.AppBarLayout;
 import com.google.android.material.appbar.CollapsingToolbarLayout;
diff --git a/packages/SettingsLib/SettingsTransition/AndroidManifest.xml b/packages/SettingsLib/SettingsTransition/AndroidManifest.xml
index b6aff53..244b367 100644
--- a/packages/SettingsLib/SettingsTransition/AndroidManifest.xml
+++ b/packages/SettingsLib/SettingsTransition/AndroidManifest.xml
@@ -16,7 +16,7 @@
 -->
 
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
-          package="com.android.settingslib.transition">
+          package="com.android.settingslib.widget">
 
     <uses-sdk android:minSdkVersion="29" />
 
diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml
index 4f84c8c..322e1be 100644
--- a/packages/SettingsLib/res/values/strings.xml
+++ b/packages/SettingsLib/res/values/strings.xml
@@ -1608,6 +1608,8 @@
     <string name="dream_complication_title_aqi">Air Quality</string>
     <!-- Screensaver overlay which displays cast info. [CHAR LIMIT=20] -->
     <string name="dream_complication_title_cast_info">Cast Info</string>
+    <!-- Screensaver overlay which displays home controls. [CHAR LIMIT=20] -->
+    <string name="dream_complication_title_home_controls">Home Controls</string>
 
     <!-- Title for a screen allowing the user to choose a profile picture. [CHAR LIMIT=NONE] -->
     <string name="avatar_picker_title">Choose a profile picture</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
index 01d0cc4..a46e232 100644
--- a/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/dream/DreamBackend.java
@@ -91,7 +91,8 @@
             COMPLICATION_TYPE_DATE,
             COMPLICATION_TYPE_WEATHER,
             COMPLICATION_TYPE_AIR_QUALITY,
-            COMPLICATION_TYPE_CAST_INFO
+            COMPLICATION_TYPE_CAST_INFO,
+            COMPLICATION_TYPE_HOME_CONTROLS
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface ComplicationType {}
@@ -101,6 +102,7 @@
     public static final int COMPLICATION_TYPE_WEATHER = 3;
     public static final int COMPLICATION_TYPE_AIR_QUALITY = 4;
     public static final int COMPLICATION_TYPE_CAST_INFO = 5;
+    public static final int COMPLICATION_TYPE_HOME_CONTROLS = 6;
 
     private final Context mContext;
     private final IDreamManager mDreamManager;
@@ -346,6 +348,9 @@
             case COMPLICATION_TYPE_CAST_INFO:
                 res = R.string.dream_complication_title_cast_info;
                 break;
+            case COMPLICATION_TYPE_HOME_CONTROLS:
+                res = R.string.dream_complication_title_home_controls;
+                break;
             default:
                 return null;
         }
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 1344144..cd3a722 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -145,25 +145,10 @@
 filegroup {
     name: "SystemUI-tests-utils",
     srcs: [
-        "tests/src/com/android/systemui/SysuiBaseFragmentTest.java",
-        "tests/src/com/android/systemui/SysuiTestCase.java",
-        "tests/src/com/android/systemui/TestableDependency.java",
-        "tests/src/com/android/systemui/classifier/FalsingManagerFake.java",
-        "tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryBuilder.java",
-        "tests/src/com/android/systemui/statusbar/RankingBuilder.java",
-        "tests/src/com/android/systemui/statusbar/SbnBuilder.java",
-        "tests/src/com/android/systemui/SysuiTestableContext.java",
-        "tests/src/com/android/systemui/util/**/*Fake.java",
-        "tests/src/com/android/systemui/utils/leaks/BaseLeakChecker.java",
-        "tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java",
-        "tests/src/com/android/systemui/**/Fake*.java",
-        "tests/src/com/android/systemui/**/Fake*.kt",
+        "tests/utils/src/**/*.java",
+        "tests/utils/src/**/*.kt",
     ],
-    exclude_srcs: [
-        "tests/src/com/android/systemui/**/*Test.java",
-        "tests/src/com/android/systemui/**/*Test.kt",
-    ],
-    path: "tests/src",
+    path: "tests/utils/src",
 }
 
 java_library {
@@ -171,8 +156,8 @@
     srcs: [
         "src/com/android/systemui/util/concurrency/DelayableExecutor.java",
         "src/com/android/systemui/util/time/SystemClock.java",
-        "tests/src/com/android/systemui/util/concurrency/FakeExecutor.java",
-        "tests/src/com/android/systemui/util/time/FakeSystemClock.java",
+        "tests/utils/src/com/android/systemui/util/concurrency/FakeExecutor.java",
+        "tests/utils/src/com/android/systemui/util/time/FakeSystemClock.java",
     ],
     jarjar_rules: ":jarjar-rules-shared",
 }
@@ -195,6 +180,7 @@
         "src/**/*.java",
         "src/**/I*.aidl",
         ":ReleaseJavaFiles",
+        ":SystemUI-tests-utils",
     ],
     static_libs: [
         "WifiTrackerLib",
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
index ff64c78..d427a57 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TextInterpolator.kt
@@ -485,7 +485,13 @@
         val out = mutableListOf<List<PositionedGlyphs>>()
         for (lineNo in 0 until layout.lineCount) { // Shape all lines.
             val lineStart = layout.getLineStart(lineNo)
-            val count = layout.getLineEnd(lineNo) - lineStart
+            var count = layout.getLineEnd(lineNo) - lineStart
+            // Do not render the last character in the line if it's a newline and unprintable
+            val last = lineStart + count - 1
+            if (last > lineStart && last < layout.text.length && layout.text[last] == '\n') {
+                count--
+            }
+
             val runs = mutableListOf<PositionedGlyphs>()
             TextShaper.shapeText(layout.text, lineStart, count, layout.textDirectionHeuristic,
                     paint) { _, _, glyphs, _ ->
diff --git a/packages/SystemUI/docs/device-entry/keyguard.md b/packages/SystemUI/docs/device-entry/keyguard.md
index 337f73b..8634c95 100644
--- a/packages/SystemUI/docs/device-entry/keyguard.md
+++ b/packages/SystemUI/docs/device-entry/keyguard.md
@@ -30,6 +30,10 @@
 
 ### How the device locks
 
+### Quick Affordances
+
+These are interactive UI elements that appear on the lockscreen when the device is locked. They allow the user to perform quick actions without unlocking their device. To learn more about them, please see [this dedicated document](quickaffordance.md)
+
 ## Debugging Tips
 Enable verbose keyguard logs that will print to logcat. Should only be used temporarily for debugging. See [KeyguardConstants][5].
 ```
diff --git a/packages/SystemUI/docs/device-entry/quickaffordance.md b/packages/SystemUI/docs/device-entry/quickaffordance.md
new file mode 100644
index 0000000..a96e5339
--- /dev/null
+++ b/packages/SystemUI/docs/device-entry/quickaffordance.md
@@ -0,0 +1,25 @@
+# Keyguard Quick Affordances
+These are interactive UI elements that appear at the bottom of the lockscreen when the device is
+locked. They allow the user to perform quick actions without unlocking their device. For example:
+opening an screen that lets them control the smart devices in their home, access their touch-to-pay
+credit card, etc.
+
+## Adding a new Quick Affordance
+### Step 1: create a new quick affordance config
+* Create a new class under the [systemui/keyguard/data/quickaffordance](../../src/com/android/systemui/keyguard/data/quickaffordance) directory
+* Please make sure that the class is injected through the Dagger dependency injection system by using the `@Inject` annotation on its main constructor and the `@SysUISingleton` annotation at class level, to make sure only one instance of the class is ever instantiated
+* Have the class implement the [KeyguardQuickAffordanceConfig](../../src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt) interface, notes:
+  * The `state` Flow property must emit `State.Hidden` when the feature is not enabled!
+  * It is safe to assume that `onQuickAffordanceClicked` will not be invoked if-and-only-if the previous rule is followed
+  * When implementing `onQuickAffordanceClicked`, the implementation can do something or it can ask the framework to start an activity using an `Intent` provided by the implementation
+* Please include a unit test for your new implementation under [the correct directory](../../tests/src/com/android/systemui/keyguard/data/quickaffordance)
+
+### Step 2: choose a position and priority
+* Add the new class as a dependency in the constructor of [KeyguardQuickAffordanceConfigs](../../src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceConfigs.kt)
+* Place the new class in one of the available positions in the `configsByPosition` property, note:
+  * In each position, there is a list. The order matters. The order of that list is the priority order in which the framework considers each config. The first config whose state property returns `State.Visible` determines the button that is shown for that position
+  * Please only add to one position. The framework treats each position individually and there is no good way to prevent the same config from making its button appear in more than one position at the same time
+
+### Step 3: manually verify the new quick affordance
+* Build and launch SysUI on a device
+* Verify that the quick affordance button for the new implementation is correctly visible and clicking it does the right thing
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index 0c191607..cafdc86 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -13,9 +13,9 @@
  */
 package com.android.systemui.plugins
 
+import android.content.res.Resources
 import android.graphics.drawable.Drawable
 import android.view.View
-import com.android.internal.colorextraction.ColorExtractor
 import com.android.systemui.plugins.annotations.ProvidesInterface
 import java.io.PrintWriter
 import java.util.Locale
@@ -57,7 +57,15 @@
     val events: ClockEvents
 
     /** Triggers for various animations */
-    val animation: ClockAnimation
+    val animations: ClockAnimations
+
+    /** Initializes various rendering parameters. If never called, provides reasonable defaults. */
+    fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
+        events.onColorPaletteChanged(resources)
+        animations.doze(dozeFraction)
+        animations.fold(foldFraction)
+        events.onTimeTick()
+    }
 
     /** Optional method for dumping debug information */
     fun dump(pw: PrintWriter) { }
@@ -80,15 +88,12 @@
     /** Call whenever font settings change */
     fun onFontSettingChanged() { }
 
-    /** Call whenever the color pallete should update */
-    fun onColorPaletteChanged(palette: ColorExtractor.GradientColors) { }
+    /** Call whenever the color palette should update */
+    fun onColorPaletteChanged(resources: Resources) { }
 }
 
 /** Methods which trigger various clock animations */
-interface ClockAnimation {
-    /** Initializes the doze & fold animation positions. Defaults to neither folded nor dozing. */
-    fun initialize(dozeFraction: Float, foldFraction: Float) { }
-
+interface ClockAnimations {
     /** Runs an enter animation (if any) */
     fun enter() { }
 
diff --git a/packages/SystemUI/proguard.flags b/packages/SystemUI/proguard.flags
index 5b9299c..7538555 100644
--- a/packages/SystemUI/proguard.flags
+++ b/packages/SystemUI/proguard.flags
@@ -11,6 +11,11 @@
 -keep class * extends com.android.systemui.CoreStartable
 -keep class * implements com.android.systemui.CoreStartable$Injector
 
+# Needed for builds to properly initialize KeyFrames from xml scene
+-keepclassmembers class * extends androidx.constraintlayout.motion.widget.Key {
+  public <init>();
+}
+
 -keepclasseswithmembers class * {
     public <init>(android.content.Context, android.util.AttributeSet);
 }
diff --git a/packages/SystemUI/res-keyguard/font/clock.xml b/packages/SystemUI/res-keyguard/font/clock.xml
deleted file mode 100644
index 0137dc3..0000000
--- a/packages/SystemUI/res-keyguard/font/clock.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-**
-** Copyright 2020, 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.
-*/
--->
-
-<!--
-** AOD/LockScreen Clock font.
-** Should include all numeric glyphs in all supported locales.
-** Recommended: font with variable width to support AOD => LS animations
--->
-<!-- TODO: Remove when clock migration complete -->
-<font-family xmlns:android="http://schemas.android.com/apk/res/android">
-    <font android:typeface="monospace"/>
-</font-family>
\ No newline at end of file
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
index 6a38507..8b8ebf0 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_clock_switch.xml
@@ -31,42 +31,14 @@
         android:layout_alignParentStart="true"
         android:layout_alignParentTop="true"
         android:paddingStart="@dimen/clock_padding_start">
-        <com.android.systemui.shared.clocks.AnimatableClockView
-            android:id="@+id/animatable_clock_view"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="start"
-            android:gravity="start"
-            android:textSize="@dimen/clock_text_size"
-            android:fontFamily="@font/clock"
-            android:elegantTextHeight="false"
-            android:singleLine="true"
-            android:fontFeatureSettings="pnum"
-            chargeAnimationDelay="350"
-            dozeWeight="200"
-            lockScreenWeight="400"
-        />
     </FrameLayout>
     <FrameLayout
         android:id="@+id/lockscreen_clock_view_large"
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
+        android:layout_height="match_parent"
         android:layout_below="@id/keyguard_slice_view"
+        android:paddingTop="@dimen/keyguard_large_clock_top_padding"
         android:visibility="gone">
-        <com.android.systemui.shared.clocks.AnimatableClockView
-            android:id="@+id/animatable_clock_view_large"
-            android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
-            android:layout_gravity="center"
-            android:gravity="center_horizontal"
-            android:textSize="@dimen/large_clock_text_size"
-            android:fontFamily="@font/clock"
-            android:typeface="monospace"
-            android:elegantTextHeight="false"
-            chargeAnimationDelay="200"
-            dozeWeight="200"
-            lockScreenWeight="400"
-        />
     </FrameLayout>
 
     <!-- Not quite optimal but needed to translate these items as a group. The
diff --git a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
index 7a57293..5f4e310 100644
--- a/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_complication_clock_time.xml
@@ -19,7 +19,7 @@
     android:id="@+id/time_view"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
-    android:fontFamily="@font/clock"
+    android:fontFamily="@*android:string/config_clockFontFamily"
     android:includeFontPadding="false"
     android:textColor="@android:color/white"
     android:format12Hour="@string/dream_time_complication_12_hr_time_format"
diff --git a/packages/SystemUI/res/layout/keyguard_bottom_area.xml b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
index 12dfa10..0ca19d9 100644
--- a/packages/SystemUI/res/layout/keyguard_bottom_area.xml
+++ b/packages/SystemUI/res/layout/keyguard_bottom_area.xml
@@ -60,6 +60,30 @@
     </LinearLayout>
 
     <ImageView
+        android:id="@+id/start_button"
+        android:layout_height="@dimen/keyguard_affordance_fixed_height"
+        android:layout_width="@dimen/keyguard_affordance_fixed_width"
+        android:layout_gravity="bottom|start"
+        android:scaleType="center"
+        android:tint="?android:attr/textColorPrimary"
+        android:background="@drawable/keyguard_bottom_affordance_bg"
+        android:layout_marginStart="@dimen/keyguard_affordance_horizontal_offset"
+        android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
+        android:visibility="gone" />
+
+    <ImageView
+        android:id="@+id/end_button"
+        android:layout_height="@dimen/keyguard_affordance_fixed_height"
+        android:layout_width="@dimen/keyguard_affordance_fixed_width"
+        android:layout_gravity="bottom|end"
+        android:scaleType="center"
+        android:tint="?android:attr/textColorPrimary"
+        android:background="@drawable/keyguard_bottom_affordance_bg"
+        android:layout_marginEnd="@dimen/keyguard_affordance_horizontal_offset"
+        android:layout_marginBottom="@dimen/keyguard_affordance_vertical_offset"
+        android:visibility="gone" />
+
+    <ImageView
         android:id="@+id/wallet_button"
         android:layout_height="@dimen/keyguard_affordance_fixed_height"
         android:layout_width="@dimen/keyguard_affordance_fixed_width"
diff --git a/packages/SystemUI/res/values-sw600dp/config.xml b/packages/SystemUI/res/values-sw600dp/config.xml
index 36cc0ad..c0071cb 100644
--- a/packages/SystemUI/res/values-sw600dp/config.xml
+++ b/packages/SystemUI/res/values-sw600dp/config.xml
@@ -40,4 +40,6 @@
 
     <bool name="config_use_large_screen_shade_header">true</bool>
 
+    <!-- Whether to show the side fps hint while on bouncer -->
+    <bool name="config_show_sidefps_hint_on_bouncer">true</bool>
 </resources>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 82a3b58..ec22c60 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -548,6 +548,9 @@
     <!-- Package name of the preferred system app to perform eSOS action -->
     <string name="config_preferredEmergencySosPackage" translatable="false"></string>
 
+    <!-- Whether to show the side fps hint while on bouncer -->
+    <bool name="config_show_sidefps_hint_on_bouncer">false</bool>
+
     <!-- Whether to use the split 2-column notification shade -->
     <bool name="config_use_split_notification_shade">false</bool>
 
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 3fb00a3..205b117 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -664,13 +664,7 @@
     <!-- When large clock is showing, offset the smartspace by this amount -->
     <dimen name="keyguard_smartspace_top_offset">12dp</dimen>
     <!-- With the large clock, move up slightly from the center -->
-    <dimen name="keyguard_large_clock_top_margin">-60dp</dimen>
-
-    <!-- TODO: Remove during migration -->
-    <!-- Default line spacing multiplier between hours and minutes of the keyguard clock -->
-    <item name="keyguard_clock_line_spacing_scale" type="dimen" format="float">.7</item>
-    <!-- Burmese line spacing multiplier between hours and minutes of the keyguard clock -->
-    <item name="keyguard_clock_line_spacing_scale_burmese" type="dimen" format="float">1</item>
+    <dimen name="keyguard_large_clock_top_padding">100dp</dimen>
 
     <dimen name="notification_scrim_corner_radius">32dp</dimen>
 
@@ -890,11 +884,6 @@
          burn-in on AOD. -->
     <dimen name="burn_in_prevention_offset_y_clock">42dp</dimen>
 
-    <!-- Clock maximum font size (dp is intentional, to prevent any further scaling) -->
-     <!-- TODO: Remove when clock migration complete -->
-    <dimen name="large_clock_text_size">150dp</dimen>
-    <dimen name="clock_text_size">86dp</dimen>
-
     <!-- The maximum offset in either direction that icons move to prevent burn-in on AOD. -->
     <dimen name="default_burn_in_prevention_offset">15dp</dimen>
 
diff --git a/packages/SystemUI/shared/res/layout/clock_default_large.xml b/packages/SystemUI/shared/res/layout/clock_default_large.xml
index 8510a0a..0139d50 100644
--- a/packages/SystemUI/shared/res/layout/clock_default_large.xml
+++ b/packages/SystemUI/shared/res/layout/clock_default_large.xml
@@ -18,7 +18,6 @@
 -->
 <com.android.systemui.shared.clocks.AnimatableClockView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/animatable_clock_view_large"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="center"
diff --git a/packages/SystemUI/shared/res/layout/clock_default_small.xml b/packages/SystemUI/shared/res/layout/clock_default_small.xml
index ec0e427..390ff5e 100644
--- a/packages/SystemUI/shared/res/layout/clock_default_small.xml
+++ b/packages/SystemUI/shared/res/layout/clock_default_small.xml
@@ -18,7 +18,6 @@
 -->
 <com.android.systemui.shared.clocks.AnimatableClockView
     xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/animatable_clock_view"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:layout_gravity="start"
@@ -26,6 +25,7 @@
     android:textSize="@dimen/small_clock_text_size"
     android:fontFamily="@*android:string/config_clockFontFamily"
     android:elegantTextHeight="false"
+    android:ellipsize="none"
     android:singleLine="true"
     android:fontFeatureSettings="pnum"
     chargeAnimationDelay="350"
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
index 2739d59..8f1959e 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/AnimatableClockView.kt
@@ -20,12 +20,15 @@
 import android.annotation.FloatRange
 import android.annotation.IntRange
 import android.annotation.SuppressLint
+import android.app.compat.ChangeIdStateCache.invalidate
 import android.content.Context
 import android.graphics.Canvas
 import android.text.TextUtils
 import android.text.format.DateFormat
 import android.util.AttributeSet
 import android.widget.TextView
+import com.android.internal.R.attr.contentDescription
+import com.android.internal.R.attr.format
 import com.android.systemui.animation.GlyphCallback
 import com.android.systemui.animation.Interpolators
 import com.android.systemui.animation.TextAnimator
@@ -75,6 +78,12 @@
     val lockScreenWeight: Int
         get() = if (useBoldedVersion()) lockScreenWeightInternal + 100 else lockScreenWeightInternal
 
+    /**
+     * The number of pixels below the baseline. For fonts that support languages such as
+     * Burmese, this space can be significant and should be accounted for when computing layout.
+     */
+    val bottom get() = paint?.fontMetrics?.bottom ?: 0f
+
     init {
         val animatableClockViewAttributes = context.obtainStyledAttributes(
             attrs, R.styleable.AnimatableClockView, defStyleAttr, defStyleRes
@@ -133,6 +142,15 @@
         // relayout if the text didn't actually change.
         if (!TextUtils.equals(text, formattedText)) {
             text = formattedText
+
+            // Because the TextLayout may mutate under the hood as a result of the new text, we
+            // notify the TextAnimator that it may have changed and request a measure/layout. A
+            // crash will occur on the next invocation of setTextStyle if the layout is mutated
+            // without being notified TextInterpolator being notified.
+            if (layout != null) {
+                textAnimator?.updateLayout(layout)
+            }
+            requestLayout()
         }
     }
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
index a4c03b0..835d6e9 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/ClockRegistry.kt
@@ -35,8 +35,6 @@
 private val TAG = ClockRegistry::class.simpleName
 private val DEBUG = true
 
-typealias ClockChangeListener = () -> Unit
-
 /** ClockRegistry aggregates providers and plugins */
 open class ClockRegistry(
     val context: Context,
@@ -51,12 +49,19 @@
         defaultClockProvider: DefaultClockProvider
     ) : this(context, pluginManager, handler, defaultClockProvider as ClockProvider) { }
 
+    // Usually this would be a typealias, but a SAM provides better java interop
+    fun interface ClockChangeListener {
+        fun onClockChanged()
+    }
+
+    var isEnabled: Boolean = false
+
     private val gson = Gson()
     private val availableClocks = mutableMapOf<ClockId, ClockInfo>()
     private val clockChangeListeners = mutableListOf<ClockChangeListener>()
     private val settingObserver = object : ContentObserver(handler) {
         override fun onChange(selfChange: Boolean, uris: Collection<Uri>, flags: Int, userId: Int) =
-            clockChangeListeners.forEach { it() }
+            clockChangeListeners.forEach { it.onClockChanged() }
     }
 
     private val pluginListener = object : PluginListener<ClockProviderPlugin> {
@@ -69,22 +74,37 @@
 
     open var currentClockId: ClockId
         get() {
-            val json = Settings.Secure.getString(
-                context.contentResolver,
-                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
-            )
-            return gson.fromJson(json, ClockSetting::class.java).clockId
+            return try {
+                val json = Settings.Secure.getString(
+                    context.contentResolver,
+                    Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE
+                )
+                gson.fromJson(json, ClockSetting::class.java)?.clockId ?: DEFAULT_CLOCK_ID
+            } catch (ex: Exception) {
+                Log.e(TAG, "Failed to parse clock setting", ex)
+                DEFAULT_CLOCK_ID
+            }
         }
         set(value) {
-            val json = gson.toJson(ClockSetting(value, System.currentTimeMillis()))
-            Settings.Secure.putString(
-                context.contentResolver,
-                Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
-            )
+            try {
+                val json = gson.toJson(ClockSetting(value, System.currentTimeMillis()))
+                Settings.Secure.putString(
+                    context.contentResolver,
+                    Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, json
+                )
+            } catch (ex: Exception) {
+                Log.e(TAG, "Failed to set clock setting", ex)
+            }
         }
 
     init {
         connectClocks(defaultClockProvider)
+        if (!availableClocks.containsKey(DEFAULT_CLOCK_ID)) {
+            throw IllegalArgumentException(
+                "$defaultClockProvider did not register clock at $DEFAULT_CLOCK_ID"
+            )
+        }
+
         pluginManager.addPluginListener(pluginListener, ClockProviderPlugin::class.java)
         context.contentResolver.registerContentObserver(
             Settings.Secure.getUriFor(Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE),
@@ -100,8 +120,11 @@
             val id = clock.clockId
             val current = availableClocks[id]
             if (current != null) {
-                Log.e(TAG, "Clock Id conflict: $id is registered by both " +
-                    "${provider::class.simpleName} and ${current.provider::class.simpleName}")
+                Log.e(
+                    TAG,
+                    "Clock Id conflict: $id is registered by both " +
+                        "${provider::class.simpleName} and ${current.provider::class.simpleName}"
+                )
                 return
             }
 
@@ -110,7 +133,7 @@
                 if (DEBUG) {
                     Log.i(TAG, "Current clock ($currentId) was connected")
                 }
-                clockChangeListeners.forEach { it() }
+                clockChangeListeners.forEach { it.onClockChanged() }
             }
         }
     }
@@ -122,12 +145,17 @@
 
             if (currentId == clock.clockId) {
                 Log.w(TAG, "Current clock ($currentId) was disconnected")
-                clockChangeListeners.forEach { it() }
+                clockChangeListeners.forEach { it.onClockChanged() }
             }
         }
     }
 
-    fun getClocks(): List<ClockMetadata> = availableClocks.map { (_, clock) -> clock.metadata }
+    fun getClocks(): List<ClockMetadata> {
+        if (!isEnabled) {
+            return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata)
+        }
+        return availableClocks.map { (_, clock) -> clock.metadata }
+    }
 
     fun getClockThumbnail(clockId: ClockId): Drawable? =
         availableClocks[clockId]?.provider?.getClockThumbnail(clockId)
@@ -142,7 +170,7 @@
 
     fun createCurrentClock(): Clock {
         val clockId = currentClockId
-        if (!clockId.isNullOrEmpty()) {
+        if (isEnabled && clockId.isNotEmpty()) {
             val clock = createClock(clockId)
             if (clock != null) {
                 return clock
@@ -164,6 +192,6 @@
 
     private data class ClockSetting(
         val clockId: ClockId,
-        val _applied_timestamp: Long
+        val _applied_timestamp: Long?
     )
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 5d8da59..1d8abe3 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -19,10 +19,9 @@
 import android.icu.text.NumberFormat
 import android.util.TypedValue
 import android.view.LayoutInflater
-import com.android.internal.colorextraction.ColorExtractor
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.plugins.Clock
-import com.android.systemui.plugins.ClockAnimation
+import com.android.systemui.plugins.ClockAnimations
 import com.android.systemui.plugins.ClockEvents
 import com.android.systemui.plugins.ClockId
 import com.android.systemui.plugins.ClockMetadata
@@ -102,10 +101,13 @@
                 TypedValue.COMPLEX_UNIT_PX,
                 resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
             )
+            recomputePadding()
         }
 
-        override fun onColorPaletteChanged(palette: ColorExtractor.GradientColors) =
-            clocks.forEach { it.setColors(DOZE_COLOR, palette.mainColor) }
+        override fun onColorPaletteChanged(resources: Resources) {
+            val color = resources.getColor(android.R.color.system_accent1_100)
+            clocks.forEach { it.setColors(DOZE_COLOR, color) }
+        }
 
         override fun onLocaleChanged(locale: Locale) {
             val nf = NumberFormat.getInstance(locale)
@@ -119,8 +121,17 @@
         }
     }
 
-    override val animation = object : ClockAnimation {
-        override fun initialize(dozeFraction: Float, foldFraction: Float) {
+    override var animations = DefaultClockAnimations(0f, 0f)
+        private set
+
+    inner class DefaultClockAnimations(
+        dozeFraction: Float,
+        foldFraction: Float
+    ) : ClockAnimations {
+        private var foldState = AnimationState(0f)
+        private var dozeState = AnimationState(0f)
+
+        init {
             dozeState = AnimationState(dozeFraction)
             foldState = AnimationState(foldFraction)
 
@@ -132,14 +143,13 @@
         }
 
         override fun enter() {
-            if (dozeState.isActive) {
+            if (!dozeState.isActive) {
                 clocks.forEach { it.animateAppearOnLockscreen() }
             }
         }
 
         override fun charge() = clocks.forEach { it.animateCharge { dozeState.isActive } }
 
-        private var foldState = AnimationState(0f)
         override fun fold(fraction: Float) {
             val (hasChanged, hasJumped) = foldState.update(fraction)
             if (hasChanged) {
@@ -147,7 +157,6 @@
             }
         }
 
-        private var dozeState = AnimationState(0f)
         override fun doze(fraction: Float) {
             val (hasChanged, hasJumped) = dozeState.update(fraction)
             if (hasChanged) {
@@ -172,6 +181,19 @@
 
     init {
         events.onLocaleChanged(Locale.getDefault())
+        clocks.forEach { it.setColors(DOZE_COLOR, DOZE_COLOR) }
+    }
+
+    override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
+        recomputePadding()
+        animations = DefaultClockAnimations(dozeFraction, foldFraction)
+        events.onColorPaletteChanged(resources)
+        events.onTimeTick()
+    }
+
+    private fun recomputePadding() {
+        val topPadding = -1 * (largeClock.bottom.toInt() - 180)
+        largeClock.setPadding(0, topPadding, 0, 0)
     }
 
     override fun dump(pw: PrintWriter) = clocks.forEach { it.dump(pw) }
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 afa255a..ef9e095 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
@@ -79,6 +79,8 @@
     // Fields used only to unrap into RemoteAnimationTarget
     private final Rect startBounds;
 
+    public final boolean willShowImeOnTarget;
+
     public RemoteAnimationTargetCompat(RemoteAnimationTarget app) {
         taskId = app.taskId;
         mode = app.mode;
@@ -102,6 +104,7 @@
         windowType = app.windowType;
         windowConfiguration = app.windowConfiguration;
         startBounds = app.startBounds;
+        willShowImeOnTarget = app.willShowImeOnTarget;
     }
 
     private static int newModeToLegacyMode(int newMode) {
@@ -118,14 +121,15 @@
     }
 
     public RemoteAnimationTarget unwrap() {
-        return new RemoteAnimationTarget(
+        final RemoteAnimationTarget target = new RemoteAnimationTarget(
                 taskId, mode, leash, isTranslucent, clipRect, contentInsets,
                 prefixOrderIndex, position, localBounds, screenSpaceBounds, windowConfiguration,
                 isNotInRecents, mStartLeash, startBounds, taskInfo, allowEnterPip, windowType
         );
+        target.setWillShowImeOnTarget(willShowImeOnTarget);
+        return target;
     }
 
-
     /**
      * Almost a copy of Transitions#setupStartState.
      * TODO: remove when there is proper cross-process transaction sync.
@@ -234,6 +238,7 @@
             ? change.getTaskInfo().configuration.windowConfiguration
             : new WindowConfiguration();
         startBounds = change.getStartAbsBounds();
+        willShowImeOnTarget = (change.getFlags() & TransitionInfo.FLAG_WILL_IME_SHOWN) != 0;
     }
 
     public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) {
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 609846e..7c1ef8c 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
@@ -230,6 +230,7 @@
         private IBinder mTransition = null;
         private boolean mKeyguardLocked = false;
         private RemoteAnimationTargetCompat[] mAppearedTargets;
+        private boolean mWillFinishToHome = false;
 
         void setup(RecentsAnimationControllerCompat wrapped, TransitionInfo info,
                 IRemoteTransitionFinishedCallback finishCB,
@@ -392,7 +393,7 @@
                 if (toHome) wct.reorder(mRecentsTask, true /* toTop */);
                 else wct.restoreTransientOrder(mRecentsTask);
             }
-            if (!toHome && mPausingTasks != null && mOpeningLeashes == null) {
+            if (!toHome && !mWillFinishToHome && mPausingTasks != null && mOpeningLeashes == null) {
                 // The gesture went back to opening the app rather than continuing with
                 // recents, so end the transition by moving the app back to the top (and also
                 // re-showing it's task).
@@ -476,6 +477,7 @@
         }
 
         @Override public void setWillFinishToHome(boolean willFinishToHome) {
+            mWillFinishToHome = willFinishToHome;
             if (mWrapped != null) mWrapped.setWillFinishToHome(willFinishToHome);
         }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
index c69ff7e..e0b11d8 100644
--- a/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
+++ b/packages/SystemUI/src/com/android/keyguard/AnimatableClockController.java
@@ -182,18 +182,6 @@
         mStatusBarStateController.removeCallback(mStatusBarStateListener);
     }
 
-    /**
-     * @return the number of pixels below the baseline. For fonts that support languages such as
-     * Burmese, this space can be significant.
-     */
-    public float getBottom() {
-        if (mView.getPaint() != null && mView.getPaint().getFontMetrics() != null) {
-            return mView.getPaint().getFontMetrics().bottom;
-        }
-
-        return 0f;
-    }
-
     /** Animate the clock appearance */
     public void animateAppear() {
         if (!mIsDozing) mView.animateAppearOnLockscreen();
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
new file mode 100644
index 0000000..efd7bcf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -0,0 +1,153 @@
+/*
+ * 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.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Resources
+import android.text.format.DateFormat
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.Clock
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
+import com.android.systemui.statusbar.policy.ConfigurationController
+import java.io.PrintWriter
+import java.util.Locale
+import java.util.TimeZone
+import javax.inject.Inject
+
+/**
+ * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
+ * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
+ */
+class ClockEventController @Inject constructor(
+    private val statusBarStateController: StatusBarStateController,
+    private val broadcastDispatcher: BroadcastDispatcher,
+    private val batteryController: BatteryController,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val configurationController: ConfigurationController,
+    @Main private val resources: Resources,
+    private val context: Context
+) {
+    var clock: Clock? = null
+        set(value) {
+            field = value
+            if (value != null) {
+                value.initialize(resources, dozeAmount, 0f)
+            }
+        }
+
+    private var isDozing = false
+        private set
+
+    private var isCharging = false
+    private var dozeAmount = 0f
+    private var isKeyguardShowing = false
+
+    private val configListener = object : ConfigurationController.ConfigurationListener {
+        override fun onThemeChanged() {
+            clock?.events?.onColorPaletteChanged(resources)
+        }
+    }
+
+    private val batteryCallback = object : BatteryStateChangeCallback {
+        override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
+            if (isKeyguardShowing && !isCharging && charging) {
+                clock?.animations?.charge()
+            }
+            isCharging = charging
+        }
+    }
+
+    private val localeBroadcastReceiver = object : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            clock?.events?.onLocaleChanged(Locale.getDefault())
+        }
+    }
+
+    private val statusBarStateListener = object : StatusBarStateController.StateListener {
+        override fun onDozeAmountChanged(linear: Float, eased: Float) {
+            clock?.animations?.doze(linear)
+
+            isDozing = linear > dozeAmount
+            dozeAmount = linear
+        }
+    }
+
+    private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
+        override fun onKeyguardVisibilityChanged(showing: Boolean) {
+            isKeyguardShowing = showing
+            if (!isKeyguardShowing) {
+                clock?.animations?.doze(if (isDozing) 1f else 0f)
+            }
+        }
+
+        override fun onTimeFormatChanged(timeFormat: String) {
+            clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+        }
+
+        override fun onTimeZoneChanged(timeZone: TimeZone) {
+            clock?.events?.onTimeZoneChanged(timeZone)
+        }
+
+        override fun onUserSwitchComplete(userId: Int) {
+            clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+        }
+    }
+
+    init {
+        isDozing = statusBarStateController.isDozing
+    }
+
+    fun registerListeners() {
+        dozeAmount = statusBarStateController.dozeAmount
+        isDozing = statusBarStateController.isDozing || dozeAmount != 0f
+
+        broadcastDispatcher.registerReceiver(
+            localeBroadcastReceiver,
+            IntentFilter(Intent.ACTION_LOCALE_CHANGED)
+        )
+        configurationController.addCallback(configListener)
+        batteryController.addCallback(batteryCallback)
+        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        statusBarStateController.addCallback(statusBarStateListener)
+    }
+
+    fun unregisterListeners() {
+        broadcastDispatcher.unregisterReceiver(localeBroadcastReceiver)
+        configurationController.removeCallback(configListener)
+        batteryController.removeCallback(batteryCallback)
+        keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
+        statusBarStateController.removeCallback(statusBarStateListener)
+    }
+
+    /**
+     * Dump information for debugging
+     */
+    fun dump(pw: PrintWriter) {
+        pw.println(this)
+        clock?.dump(pw)
+    }
+
+    companion object {
+        private val TAG = ClockEventController::class.simpleName
+        private const val FORMAT_NUMBER = 1234567890
+    }
+}
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
index 206b8be..e1fabde 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitch.java
@@ -5,10 +5,8 @@
 import android.animation.AnimatorSet;
 import android.animation.ObjectAnimator;
 import android.content.Context;
-import android.graphics.Paint;
-import android.graphics.Paint.Style;
 import android.util.AttributeSet;
-import android.util.TypedValue;
+import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
@@ -17,19 +15,14 @@
 import androidx.annotation.IntDef;
 import androidx.annotation.VisibleForTesting;
 
-import com.android.internal.colorextraction.ColorExtractor;
 import com.android.keyguard.dagger.KeyguardStatusViewScope;
 import com.android.systemui.R;
 import com.android.systemui.animation.Interpolators;
-import com.android.systemui.plugins.ClockPlugin;
-import com.android.systemui.shared.clocks.AnimatableClockView;
+import com.android.systemui.plugins.Clock;
 
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.util.Arrays;
-import java.util.TimeZone;
-
 /**
  * Switch to show plugin clock when plugin is connected, otherwise it will show default clock.
  */
@@ -50,17 +43,10 @@
     public static final int SMALL = 1;
 
     /**
-     * Optional/alternative clock injected via plugin.
-     */
-    private ClockPlugin mClockPlugin;
-
-    /**
      * Frame for small/large clocks
      */
-    private FrameLayout mClockFrame;
+    private FrameLayout mSmallClockFrame;
     private FrameLayout mLargeClockFrame;
-    private AnimatableClockView mClockView;
-    private AnimatableClockView mLargeClockView;
 
     private View mStatusArea;
     private int mSmartspaceTopOffset;
@@ -80,12 +66,6 @@
     @VisibleForTesting AnimatorSet mClockOutAnim = null;
     private ObjectAnimator mStatusAreaAnim = null;
 
-    /**
-     * If the Keyguard Slice has a header (big center-aligned text.)
-     */
-    private boolean mSupportsDarkText;
-    private int[] mColorPalette;
-
     private int mClockSwitchYAmount;
     @VisibleForTesting boolean mChildrenAreLaidOut = false;
 
@@ -97,97 +77,38 @@
      * Apply dp changes on font/scale change
      */
     public void onDensityOrFontScaleChanged() {
-        mLargeClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContext.getResources()
-                .getDimensionPixelSize(R.dimen.large_clock_text_size));
-        mClockView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mContext.getResources()
-                .getDimensionPixelSize(R.dimen.clock_text_size));
-
         mClockSwitchYAmount = mContext.getResources().getDimensionPixelSize(
                 R.dimen.keyguard_clock_switch_y_shift);
-
         mSmartspaceTopOffset = mContext.getResources().getDimensionPixelSize(
                 R.dimen.keyguard_smartspace_top_offset);
     }
 
-    /**
-     * Returns if this view is presenting a custom clock, or the default implementation.
-     */
-    public boolean hasCustomClock() {
-        return mClockPlugin != null;
-    }
-
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
 
-        mClockFrame = findViewById(R.id.lockscreen_clock_view);
-        mClockView = findViewById(R.id.animatable_clock_view);
+        mSmallClockFrame = findViewById(R.id.lockscreen_clock_view);
         mLargeClockFrame = findViewById(R.id.lockscreen_clock_view_large);
-        mLargeClockView = findViewById(R.id.animatable_clock_view_large);
         mStatusArea = findViewById(R.id.keyguard_status_area);
 
         onDensityOrFontScaleChanged();
     }
 
-    void setClockPlugin(ClockPlugin plugin, int statusBarState) {
+    void setClock(Clock clock, int statusBarState) {
         // Disconnect from existing plugin.
-        if (mClockPlugin != null) {
-            View smallClockView = mClockPlugin.getView();
-            if (smallClockView != null && smallClockView.getParent() == mClockFrame) {
-                mClockFrame.removeView(smallClockView);
-            }
-            View bigClockView = mClockPlugin.getBigClockView();
-            if (bigClockView != null && bigClockView.getParent() == mLargeClockFrame) {
-                mLargeClockFrame.removeView(bigClockView);
-            }
-            mClockPlugin.onDestroyView();
-            mClockPlugin = null;
-        }
-        if (plugin == null) {
-            mClockView.setVisibility(View.VISIBLE);
-            mLargeClockView.setVisibility(View.VISIBLE);
+        mSmallClockFrame.removeAllViews();
+        mLargeClockFrame.removeAllViews();
+
+        if (clock == null) {
+            Log.e(TAG, "No clock being shown");
             return;
         }
+
         // Attach small and big clock views to hierarchy.
-        View smallClockView = plugin.getView();
-        if (smallClockView != null) {
-            mClockFrame.addView(smallClockView, -1,
-                    new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
-                            ViewGroup.LayoutParams.WRAP_CONTENT));
-            mClockView.setVisibility(View.GONE);
-        }
-        View bigClockView = plugin.getBigClockView();
-        if (bigClockView != null) {
-            mLargeClockFrame.addView(bigClockView);
-            mLargeClockView.setVisibility(View.GONE);
-        }
-
-        // Initialize plugin parameters.
-        mClockPlugin = plugin;
-        mClockPlugin.setStyle(getPaint().getStyle());
-        mClockPlugin.setTextColor(getCurrentTextColor());
-        mClockPlugin.setDarkAmount(mDarkAmount);
-        if (mColorPalette != null) {
-            mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette);
-        }
-    }
-
-    /**
-     * It will also update plugin setStyle if plugin is connected.
-     */
-    public void setStyle(Style style) {
-        if (mClockPlugin != null) {
-            mClockPlugin.setStyle(style);
-        }
-    }
-
-    /**
-     * It will also update plugin setTextColor if plugin is connected.
-     */
-    public void setTextColor(int color) {
-        if (mClockPlugin != null) {
-            mClockPlugin.setTextColor(color);
-        }
+        mSmallClockFrame.addView(clock.getSmallClock(), -1,
+                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT));
+        mLargeClockFrame.addView(clock.getLargeClock());
     }
 
     private void updateClockViews(boolean useLargeClock, boolean animate) {
@@ -203,14 +124,14 @@
         int direction = 1;
         float statusAreaYTranslation;
         if (useLargeClock) {
-            out = mClockFrame;
+            out = mSmallClockFrame;
             in = mLargeClockFrame;
             if (indexOfChild(in) == -1) addView(in);
             direction = -1;
-            statusAreaYTranslation = mClockFrame.getTop() - mStatusArea.getTop()
+            statusAreaYTranslation = mSmallClockFrame.getTop() - mStatusArea.getTop()
                     + mSmartspaceTopOffset;
         } else {
-            in = mClockFrame;
+            in = mSmallClockFrame;
             out = mLargeClockFrame;
             statusAreaYTranslation = 0f;
 
@@ -269,18 +190,6 @@
     }
 
     /**
-     * Set the amount (ratio) that the device has transitioned to doze.
-     *
-     * @param darkAmount Amount of transition to doze: 1f for doze and 0f for awake.
-     */
-    public void setDarkAmount(float darkAmount) {
-        mDarkAmount = darkAmount;
-        if (mClockPlugin != null) {
-            mClockPlugin.setDarkAmount(darkAmount);
-        }
-    }
-
-    /**
      * Display the desired clock and hide the other one
      *
      * @return true if desired clock appeared and false if it was already visible
@@ -311,64 +220,11 @@
         mChildrenAreLaidOut = true;
     }
 
-    public Paint getPaint() {
-        return mClockView.getPaint();
-    }
-
-    public int getCurrentTextColor() {
-        return mClockView.getCurrentTextColor();
-    }
-
-    public float getTextSize() {
-        return mClockView.getTextSize();
-    }
-
-    /**
-     * Refresh the time of the clock, due to either time tick broadcast or doze time tick alarm.
-     */
-    public void refresh() {
-        if (mClockPlugin != null) {
-            mClockPlugin.onTimeTick();
-        }
-    }
-
-    /**
-     * Notifies that the time zone has changed.
-     */
-    public void onTimeZoneChanged(TimeZone timeZone) {
-        if (mClockPlugin != null) {
-            mClockPlugin.onTimeZoneChanged(timeZone);
-        }
-    }
-
-    /**
-     * Notifies that the time format has changed.
-     *
-     * @param timeFormat "12" for 12-hour format, "24" for 24-hour format
-     */
-    public void onTimeFormatChanged(String timeFormat) {
-        if (mClockPlugin != null) {
-            mClockPlugin.onTimeFormatChanged(timeFormat);
-        }
-    }
-
-    void updateColors(ColorExtractor.GradientColors colors) {
-        mSupportsDarkText = colors.supportsDarkText();
-        mColorPalette = colors.getColorPalette();
-        if (mClockPlugin != null) {
-            mClockPlugin.setColorPalette(mSupportsDarkText, mColorPalette);
-        }
-    }
-
     public void dump(PrintWriter pw, String[] args) {
         pw.println("KeyguardClockSwitch:");
-        pw.println("  mClockPlugin: " + mClockPlugin);
-        pw.println("  mClockFrame: " + mClockFrame);
+        pw.println("  mClockFrame: " + mSmallClockFrame);
         pw.println("  mLargeClockFrame: " + mLargeClockFrame);
         pw.println("  mStatusArea: " + mStatusArea);
-        pw.println("  mDarkAmount: " + mDarkAmount);
-        pw.println("  mSupportsDarkText: " + mSupportsDarkText);
-        pw.println("  mColorPalette: " + Arrays.toString(mColorPalette));
         pw.println("  mDisplayedClockSize: " + mDisplayedClockSize);
     }
 }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index 6c32a49..dd78f1c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -22,8 +22,6 @@
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
 import static com.android.keyguard.KeyguardClockSwitch.SMALL;
 
-import android.app.WallpaperManager;
-import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.os.UserHandle;
 import android.provider.Settings;
@@ -32,34 +30,30 @@
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
 
 import androidx.annotation.NonNull;
 
-import com.android.internal.colorextraction.ColorExtractor;
-import com.android.keyguard.clock.ClockManager;
 import com.android.systemui.Dumpable;
 import com.android.systemui.R;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
-import com.android.systemui.plugins.ClockPlugin;
+import com.android.systemui.plugins.Clock;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shared.clocks.ClockRegistry;
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
 import com.android.systemui.statusbar.notification.AnimatableProperty;
 import com.android.systemui.statusbar.notification.PropertyAnimator;
 import com.android.systemui.statusbar.notification.stack.AnimationProperties;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.ViewController;
 import com.android.systemui.util.settings.SecureSettings;
 
 import java.io.PrintWriter;
 import java.util.Locale;
-import java.util.TimeZone;
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
@@ -69,48 +63,24 @@
  */
 public class KeyguardClockSwitchController extends ViewController<KeyguardClockSwitch>
         implements Dumpable {
-    private static final boolean CUSTOM_CLOCKS_ENABLED = true;
-
     private final StatusBarStateController mStatusBarStateController;
-    private final SysuiColorExtractor mColorExtractor;
-    private final ClockManager mClockManager;
+    private final ClockRegistry mClockRegistry;
     private final KeyguardSliceViewController mKeyguardSliceViewController;
     private final NotificationIconAreaController mNotificationIconAreaController;
-    private final BroadcastDispatcher mBroadcastDispatcher;
-    private final BatteryController mBatteryController;
     private final LockscreenSmartspaceController mSmartspaceController;
-    private final Resources mResources;
     private final SecureSettings mSecureSettings;
     private final DumpManager mDumpManager;
+    private final ClockEventController mClockEventController;
 
-    /**
-     * Clock for both small and large sizes
-     */
-    private AnimatableClockController mClockViewController;
-    private FrameLayout mClockFrame; // top aligned clock
-    private AnimatableClockController mLargeClockViewController;
+    /** Clock frames for both small and large sizes */
+    private FrameLayout mSmallClockFrame; // top aligned clock
     private FrameLayout mLargeClockFrame; // centered clock
 
     @KeyguardClockSwitch.ClockSize
     private int mCurrentClockSize = SMALL;
 
-    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-
     private int mKeyguardClockTopMargin = 0;
-
-    /**
-     * Listener for changes to the color palette.
-     *
-     * The color palette changes when the wallpaper is changed.
-     */
-    private final ColorExtractor.OnColorsChangedListener mColorsListener =
-            (extractor, which) -> {
-                if ((which & WallpaperManager.FLAG_LOCK) != 0) {
-                    mView.updateColors(getGradientColors());
-                }
-            };
-
-    private final ClockManager.ClockChangedListener mClockChangedListener = this::setClockPlugin;
+    private final ClockRegistry.ClockChangeListener mClockChangedListener;
 
     private ViewGroup mStatusArea;
     // If set will replace keyguard_slice_view
@@ -119,9 +89,9 @@
     private final KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
 
     private boolean mOnlyClock = false;
-    private Executor mUiExecutor;
+    private final Executor mUiExecutor;
     private boolean mCanShowDoubleLineClock = true;
-    private ContentObserver mDoubleLineClockObserver = new ContentObserver(null) {
+    private final ContentObserver mDoubleLineClockObserver = new ContentObserver(null) {
         @Override
         public void onChange(boolean change) {
             updateDoubleLineClock();
@@ -142,34 +112,32 @@
     public KeyguardClockSwitchController(
             KeyguardClockSwitch keyguardClockSwitch,
             StatusBarStateController statusBarStateController,
-            SysuiColorExtractor colorExtractor,
-            ClockManager clockManager,
+            ClockRegistry clockRegistry,
             KeyguardSliceViewController keyguardSliceViewController,
             NotificationIconAreaController notificationIconAreaController,
-            BroadcastDispatcher broadcastDispatcher,
-            BatteryController batteryController,
-            KeyguardUpdateMonitor keyguardUpdateMonitor,
             LockscreenSmartspaceController smartspaceController,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             SecureSettings secureSettings,
             @Main Executor uiExecutor,
-            @Main Resources resources,
-            DumpManager dumpManager) {
+            DumpManager dumpManager,
+            ClockEventController clockEventController,
+            FeatureFlags featureFlags) {
         super(keyguardClockSwitch);
         mStatusBarStateController = statusBarStateController;
-        mColorExtractor = colorExtractor;
-        mClockManager = clockManager;
+        mClockRegistry = clockRegistry;
         mKeyguardSliceViewController = keyguardSliceViewController;
         mNotificationIconAreaController = notificationIconAreaController;
-        mBroadcastDispatcher = broadcastDispatcher;
-        mBatteryController = batteryController;
-        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
         mSmartspaceController = smartspaceController;
-        mResources = resources;
         mSecureSettings = secureSettings;
         mUiExecutor = uiExecutor;
         mKeyguardUnlockAnimationController = keyguardUnlockAnimationController;
         mDumpManager = dumpManager;
+        mClockEventController = clockEventController;
+
+        mClockRegistry.setEnabled(featureFlags.isEnabled(Flags.LOCKSCREEN_CUSTOM_CLOCKS));
+        mClockChangedListener = () -> {
+            setClock(mClockRegistry.createCurrentClock());
+        };
     }
 
     /**
@@ -186,40 +154,18 @@
     public void onInit() {
         mKeyguardSliceViewController.init();
 
-        mClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
+        mSmallClockFrame = mView.findViewById(R.id.lockscreen_clock_view);
         mLargeClockFrame = mView.findViewById(R.id.lockscreen_clock_view_large);
 
-        mClockViewController =
-                new AnimatableClockController(
-                        mView.findViewById(R.id.animatable_clock_view),
-                        mStatusBarStateController,
-                        mBroadcastDispatcher,
-                        mBatteryController,
-                        mKeyguardUpdateMonitor,
-                        mResources);
-        mClockViewController.init();
-
-        mLargeClockViewController =
-                new AnimatableClockController(
-                        mView.findViewById(R.id.animatable_clock_view_large),
-                        mStatusBarStateController,
-                        mBroadcastDispatcher,
-                        mBatteryController,
-                        mKeyguardUpdateMonitor,
-                        mResources);
-        mLargeClockViewController.init();
-
         mDumpManager.unregisterDumpable(getClass().toString()); // unregister previous clocks
         mDumpManager.registerDumpable(getClass().toString(), this);
     }
 
     @Override
     protected void onViewAttached() {
-        if (CUSTOM_CLOCKS_ENABLED) {
-            mClockManager.addOnClockChangedListener(mClockChangedListener);
-        }
-        mColorExtractor.addOnColorsChangedListener(mColorsListener);
-        mView.updateColors(getGradientColors());
+        mClockRegistry.registerClockChangeListener(mClockChangedListener);
+        setClock(mClockRegistry.createCurrentClock());
+        mClockEventController.registerListeners();
         mKeyguardClockTopMargin =
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
 
@@ -242,7 +188,6 @@
             ksv.setVisibility(View.GONE);
 
             addSmartspaceView(ksvIndex);
-            updateClockLayout();
         }
 
         mSecureSettings.registerContentObserverForUser(
@@ -264,11 +209,9 @@
 
     @Override
     protected void onViewDetached() {
-        if (CUSTOM_CLOCKS_ENABLED) {
-            mClockManager.removeOnClockChangedListener(mClockChangedListener);
-        }
-        mColorExtractor.removeOnColorsChangedListener(mColorsListener);
-        mView.setClockPlugin(null, mStatusBarStateController.getState());
+        mClockRegistry.unregisterClockChangeListener(mClockChangedListener);
+        mClockEventController.unregisterListeners();
+        setClock(null);
 
         mSecureSettings.unregisterContentObserver(mDoubleLineClockObserver);
 
@@ -307,18 +250,6 @@
         mView.onDensityOrFontScaleChanged();
         mKeyguardClockTopMargin =
                 mView.getResources().getDimensionPixelSize(R.dimen.keyguard_clock_top_margin);
-
-        updateClockLayout();
-    }
-
-    private void updateClockLayout() {
-        int largeClockTopMargin = getContext().getResources().getDimensionPixelSize(
-                R.dimen.keyguard_large_clock_top_margin)
-                - (int) mLargeClockViewController.getBottom();
-        RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(MATCH_PARENT,
-                MATCH_PARENT);
-        lp.topMargin = largeClockTopMargin;
-        mLargeClockFrame.setLayoutParams(lp);
     }
 
     /**
@@ -332,46 +263,34 @@
 
         mCurrentClockSize = clockSize;
 
+        Clock clock = getClock();
         boolean appeared = mView.switchToClock(clockSize, animate);
-        if (animate && appeared && clockSize == LARGE) {
-            mLargeClockViewController.animateAppear();
-        }
-    }
-
-    public void animateFoldToAod() {
-        if (mClockViewController != null) {
-            mClockViewController.animateFoldAppear();
-            mLargeClockViewController.animateFoldAppear();
+        if (clock != null && animate && appeared && clockSize == LARGE) {
+            clock.getAnimations().enter();
         }
     }
 
     /**
-     * If we're presenting a custom clock of just the default one.
+     * Animates the clock view between folded and unfolded states
      */
-    public boolean hasCustomClock() {
-        return mView.hasCustomClock();
-    }
-
-    /**
-     * Get the clock text size.
-     */
-    public float getClockTextSize() {
-        return mView.getTextSize();
+    public void animateFoldToAod(float foldFraction) {
+        Clock clock = getClock();
+        if (clock != null) {
+            clock.getAnimations().fold(foldFraction);
+        }
     }
 
     /**
      * Refresh clock. Called in response to TIME_TICK broadcasts.
      */
     void refresh() {
-        if (mClockViewController != null) {
-            mClockViewController.refreshTime();
-            mLargeClockViewController.refreshTime();
-        }
         if (mSmartspaceController != null) {
             mSmartspaceController.requestSmartspaceUpdate();
         }
-
-        mView.refresh();
+        Clock clock = getClock();
+        if (clock != null) {
+            clock.getEvents().onTimeTick();
+        }
     }
 
     /**
@@ -383,7 +302,7 @@
     void updatePosition(int x, float scale, AnimationProperties props, boolean animate) {
         x = getCurrentLayoutDirection() == View.LAYOUT_DIRECTION_RTL ? -x : x;
 
-        PropertyAnimator.setProperty(mClockFrame, AnimatableProperty.TRANSLATION_X,
+        PropertyAnimator.setProperty(mSmallClockFrame, AnimatableProperty.TRANSLATION_X,
                 x, props, animate);
         PropertyAnimator.setProperty(mLargeClockFrame, AnimatableProperty.SCALE_X,
                 scale, props, animate);
@@ -396,25 +315,39 @@
         }
     }
 
-    void updateTimeZone(TimeZone timeZone) {
-        mView.onTimeZoneChanged(timeZone);
-    }
-
     /**
      * Get y-bottom position of the currently visible clock on the keyguard.
      * We can't directly getBottom() because clock changes positions in AOD for burn-in
      */
     int getClockBottom(int statusBarHeaderHeight) {
+        Clock clock = getClock();
+        if (clock == null) {
+            return 0;
+        }
+
         if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
-            View clock = mLargeClockFrame.findViewById(
-                    com.android.systemui.R.id.animatable_clock_view_large);
             int frameHeight = mLargeClockFrame.getHeight();
-            int clockHeight = clock.getHeight();
+            int clockHeight = clock.getLargeClock().getHeight();
             return frameHeight / 2 + clockHeight / 2;
         } else {
-            return mClockFrame.findViewById(
-                    com.android.systemui.R.id.animatable_clock_view).getHeight()
-                    + statusBarHeaderHeight + mKeyguardClockTopMargin;
+            int clockHeight = clock.getSmallClock().getHeight();
+            return clockHeight + statusBarHeaderHeight + mKeyguardClockTopMargin;
+        }
+    }
+
+    /**
+     * Get the height of the currently visible clock on the keyguard.
+     */
+    int getClockHeight() {
+        Clock clock = getClock();
+        if (clock == null) {
+            return 0;
+        }
+
+        if (mLargeClockFrame.getVisibility() == View.VISIBLE) {
+            return clock.getLargeClock().getHeight();
+        } else {
+            return clock.getSmallClock().getHeight();
         }
     }
 
@@ -429,12 +362,13 @@
         mNotificationIconAreaController.setupAodIcons(nic);
     }
 
-    private void setClockPlugin(ClockPlugin plugin) {
-        mView.setClockPlugin(plugin, mStatusBarStateController.getState());
+    private void setClock(Clock clock) {
+        mClockEventController.setClock(clock);
+        mView.setClock(clock, mStatusBarStateController.getState());
     }
 
-    private ColorExtractor.GradientColors getGradientColors() {
-        return mColorExtractor.getColors(WallpaperManager.FLAG_LOCK);
+    private Clock getClock() {
+        return mClockEventController.getClock();
     }
 
     private int getCurrentLayoutDirection() {
@@ -467,8 +401,10 @@
     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
         pw.println("currentClockSizeLarge=" + (mCurrentClockSize == LARGE));
         pw.println("mCanShowDoubleLineClock=" + mCanShowDoubleLineClock);
-        mClockViewController.dump(pw);
-        mLargeClockViewController.dump(pw);
+        Clock clock = getClock();
+        if (clock != null) {
+            clock.dump(pw);
+        }
     }
 }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 5ee659b..0b3fe46 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -343,7 +343,8 @@
         if (!mSidefpsController.isPresent()) {
             return;
         }
-        if (mBouncerVisible && mView.isSidedSecurityMode()
+        if (mBouncerVisible
+                && getResources().getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)
                 && mUpdateMonitor.isFingerprintDetectionRunning()
                 && !mUpdateMonitor.userNeedsStrongAuth()) {
             mSidefpsController.get().show();
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
index cb3172d..83e23bd 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusView.java
@@ -17,14 +17,11 @@
 package com.android.keyguard;
 
 import android.content.Context;
-import android.graphics.Color;
 import android.util.AttributeSet;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.GridLayout;
 
-import androidx.core.graphics.ColorUtils;
-
 import com.android.systemui.R;
 import com.android.systemui.statusbar.CrossFadeHelper;
 
@@ -46,7 +43,6 @@
     private View mMediaHostContainer;
 
     private float mDarkAmount = 0;
-    private int mTextColor;
 
     public KeyguardStatusView(Context context) {
         this(context, null, 0);
@@ -71,7 +67,6 @@
         }
 
         mKeyguardSlice = findViewById(R.id.keyguard_slice_view);
-        mTextColor = mClockView.getCurrentTextColor();
 
         mMediaHostContainer = findViewById(R.id.status_view_media_container);
 
@@ -83,15 +78,12 @@
             return;
         }
         mDarkAmount = darkAmount;
-        mClockView.setDarkAmount(darkAmount);
         CrossFadeHelper.fadeOut(mMediaHostContainer, darkAmount);
         updateDark();
     }
 
     void updateDark() {
-        final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
         mKeyguardSlice.setDarkAmount(mDarkAmount);
-        mClockView.setTextColor(blendedTextColor);
     }
 
     /** Sets a translationY value on every child view except for the media view. */
@@ -113,7 +105,6 @@
     public void dump(PrintWriter pw, String[] args) {
         pw.println("KeyguardStatusView:");
         pw.println("  mDarkAmount: " + mDarkAmount);
-        pw.println("  mTextColor: " + Integer.toHexString(mTextColor));
         if (mClockView != null) {
             mClockView.dump(pw, args);
         }
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
index 014d082..c715a4e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardStatusViewController.java
@@ -30,8 +30,6 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.ViewController;
 
-import java.util.TimeZone;
-
 import javax.inject.Inject;
 
 /**
@@ -96,13 +94,6 @@
     }
 
     /**
-     * The amount we're in doze.
-     */
-    public void setDarkAmount(float darkAmount) {
-        mView.setDarkAmount(darkAmount);
-    }
-
-    /**
      * Set which clock should be displayed on the keyguard. The other one will be automatically
      * hidden.
      */
@@ -114,16 +105,10 @@
      * Performs fold to aod animation of the clocks (changes font weight from bold to thin).
      * This animation is played when AOD is enabled and foldable device is fully folded, it is
      * displayed on the outer screen
+     * @param foldFraction current fraction of fold animation complete
      */
-    public void animateFoldToAod() {
-        mKeyguardClockSwitchController.animateFoldToAod();
-    }
-
-    /**
-     * If we're presenting a custom clock of just the default one.
-     */
-    public boolean hasCustomClock() {
-        return mKeyguardClockSwitchController.hasCustomClock();
+    public void animateFoldToAod(float foldFraction) {
+        mKeyguardClockSwitchController.animateFoldToAod(foldFraction);
     }
 
     /**
@@ -143,24 +128,11 @@
     }
 
     /**
-     * Set pivot x.
+     * Update the pivot position based on the parent view
      */
-    public void setPivotX(float pivot) {
-        mView.setPivotX(pivot);
-    }
-
-    /**
-     * Set pivot y.
-     */
-    public void setPivotY(float pivot) {
-        mView.setPivotY(pivot);
-    }
-
-    /**
-     * Get the clock text size.
-     */
-    public float getClockTextSize() {
-        return mKeyguardClockSwitchController.getClockTextSize();
+    public void updatePivot(float parentWidth, float parentHeight) {
+        mView.setPivotX(parentWidth / 2f);
+        mView.setPivotY(mKeyguardClockSwitchController.getClockHeight() / 2f);
     }
 
     /**
@@ -240,11 +212,6 @@
         }
 
         @Override
-        public void onTimeZoneChanged(TimeZone timeZone) {
-            mKeyguardClockSwitchController.updateTimeZone(timeZone);
-        }
-
-        @Override
         public void onKeyguardVisibilityChanged(boolean showing) {
             if (showing) {
                 if (DEBUG) Slog.v(TAG, "refresh statusview showing:" + showing);
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 4ae2cad..2b44199 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -66,7 +66,6 @@
 import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
 import android.nfc.NfcAdapter;
-import android.os.Build;
 import android.os.CancellationSignal;
 import android.os.Handler;
 import android.os.IRemoteCallback;
@@ -96,6 +95,7 @@
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.util.LatencyTracker;
 import com.android.internal.widget.LockPatternUtils;
+import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
 import com.android.settingslib.WirelessUtils;
 import com.android.settingslib.fuelgauge.BatteryStatus;
 import com.android.systemui.Dumpable;
@@ -141,12 +141,6 @@
 public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpable {
 
     private static final String TAG = "KeyguardUpdateMonitor";
-    private static final boolean DEBUG = KeyguardConstants.DEBUG;
-    private static final boolean DEBUG_SIM_STATES = KeyguardConstants.DEBUG_SIM_STATES;
-    private static final boolean DEBUG_FACE = Build.IS_DEBUGGABLE;
-    private static final boolean DEBUG_FINGERPRINT = Build.IS_DEBUGGABLE;
-    private static final boolean DEBUG_ACTIVE_UNLOCK = Build.IS_DEBUGGABLE;
-    private static final boolean DEBUG_SPEW = false;
     private static final int BIOMETRIC_LOCKOUT_RESET_DELAY_MS = 600;
 
     // Callback messages
@@ -241,6 +235,7 @@
     }
 
     private final Context mContext;
+    private final KeyguardUpdateMonitorLogger mLogger;
     private final boolean mIsPrimaryUser;
     private final AuthController mAuthController;
     private final StatusBarStateController mStatusBarStateController;
@@ -337,17 +332,9 @@
     private static final int HAL_ERROR_RETRY_TIMEOUT = 500; // ms
     private static final int HAL_ERROR_RETRY_MAX = 20;
 
-    private final Runnable mFpCancelNotReceived = () -> {
-        Log.e(TAG, "Fp cancellation not received, transitioning to STOPPED");
-        mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
-        updateFingerprintListeningState(BIOMETRIC_ACTION_STOP);
-    };
+    private final Runnable mFpCancelNotReceived = this::onFingerprintCancelNotReceived;
 
-    private final Runnable mFaceCancelNotReceived = () -> {
-        Log.e(TAG, "Face cancellation not received, transitioning to STOPPED");
-        mFaceRunningState = BIOMETRIC_STATE_STOPPED;
-        updateFaceListeningState(BIOMETRIC_ACTION_STOP);
-    };
+    private final Runnable mFaceCancelNotReceived = this::onFaceCancelNotReceived;
 
     private final Handler mHandler;
 
@@ -470,17 +457,15 @@
 
     private void handleSimSubscriptionInfoChanged() {
         Assert.isMainThread();
-        if (DEBUG_SIM_STATES) {
-            Log.v(TAG, "onSubscriptionInfoChanged()");
-            List<SubscriptionInfo> sil = mSubscriptionManager
-                    .getCompleteActiveSubscriptionInfoList();
-            if (sil != null) {
-                for (SubscriptionInfo subInfo : sil) {
-                    Log.v(TAG, "SubInfo:" + subInfo);
-                }
-            } else {
-                Log.v(TAG, "onSubscriptionInfoChanged: list is null");
+        mLogger.v("onSubscriptionInfoChanged()");
+        List<SubscriptionInfo> sil = mSubscriptionManager
+                .getCompleteActiveSubscriptionInfoList();
+        if (sil != null) {
+            for (SubscriptionInfo subInfo : sil) {
+                mLogger.logSubInfo(subInfo);
             }
+        } else {
+            mLogger.v("onSubscriptionInfoChanged: list is null");
         }
         List<SubscriptionInfo> subscriptionInfos = getSubscriptionInfo(true /* forceReload */);
 
@@ -504,8 +489,7 @@
         while (iter.hasNext()) {
             Map.Entry<Integer, SimData> simData = iter.next();
             if (!activeSubIds.contains(simData.getKey())) {
-                Log.i(TAG, "Previously active sub id " + simData.getKey() + " is now invalid, "
-                        + "will remove");
+                mLogger.logInvalidSubId(simData.getKey());
                 iter.remove();
 
                 SimData data = simData.getValue();
@@ -690,7 +674,7 @@
             try {
                 mDreamManager.awaken();
             } catch (RemoteException e) {
-                Log.e(TAG, "Unable to awaken from dream");
+                mLogger.logException(e, "Unable to awaken from dream");
             }
         }
     }
@@ -774,15 +758,15 @@
             try {
                 userId = ActivityManager.getService().getCurrentUser().id;
             } catch (RemoteException e) {
-                Log.e(TAG, "Failed to get current user id: ", e);
+                mLogger.logException(e, "Failed to get current user id");
                 return;
             }
             if (userId != authUserId) {
-                Log.d(TAG, "Fingerprint authenticated for wrong user: " + authUserId);
+                mLogger.logFingerprintAuthForWrongUser(authUserId);
                 return;
             }
             if (isFingerprintDisabled(userId)) {
-                Log.d(TAG, "Fingerprint disabled by DPM for userId: " + userId);
+                mLogger.logFingerprintDisabledForUser(userId);
                 return;
             }
             onFingerprintAuthenticated(userId, isStrongBiometric);
@@ -805,8 +789,7 @@
     private Runnable mRetryFingerprintAuthentication = new Runnable() {
         @Override
         public void run() {
-            Log.w(TAG,
-                    "Retrying fingerprint attempt: " + mHardwareFingerprintUnavailableRetryCount);
+            mLogger.logRetryAfterFpHwUnavailable(mHardwareFingerprintUnavailableRetryCount);
             if (mFpm.isHardwareDetected()) {
                 updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
             } else if (mHardwareFingerprintUnavailableRetryCount < HAL_ERROR_RETRY_MAX) {
@@ -816,6 +799,12 @@
         }
     };
 
+    private void onFingerprintCancelNotReceived() {
+        mLogger.e("Fp cancellation not received, transitioning to STOPPED");
+        mFingerprintRunningState = BIOMETRIC_STATE_STOPPED;
+        KeyguardUpdateMonitor.this.updateFingerprintListeningState(BIOMETRIC_ACTION_STOP);
+    }
+
     private void handleFingerprintError(int msgId, String errString) {
         Assert.isMainThread();
         if (mHandler.hasCallbacks(mFpCancelNotReceived)) {
@@ -843,7 +832,7 @@
         if (msgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT) {
             lockedOutStateChanged |= !mFingerprintLockedOutPermanent;
             mFingerprintLockedOutPermanent = true;
-            Log.d(TAG, "Fingerprint locked out - requiring strong auth");
+            mLogger.d("Fingerprint locked out - requiring strong auth");
             mLockPatternUtils.requireStrongAuth(
                     STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, getCurrentUser());
         }
@@ -871,7 +860,7 @@
     }
 
     private void handleFingerprintLockoutReset(@LockoutMode int mode) {
-        Log.d(TAG, "handleFingerprintLockoutReset: " + mode);
+        mLogger.logFingerprintLockoutReset(mode);
         final boolean wasLockout = mFingerprintLockedOut;
         final boolean wasLockoutPermanent = mFingerprintLockedOutPermanent;
         mFingerprintLockedOut = (mode == BIOMETRIC_LOCKOUT_TIMED)
@@ -902,7 +891,7 @@
         boolean wasRunning = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING;
         boolean isRunning = fingerprintRunningState == BIOMETRIC_STATE_RUNNING;
         mFingerprintRunningState = fingerprintRunningState;
-        Log.d(TAG, "fingerprintRunningState: " + mFingerprintRunningState);
+        mLogger.logFingerprintRunningState(mFingerprintRunningState);
         // Clients of KeyguardUpdateMonitor don't care about the internal state about the
         // asynchronousness of the cancel cycle. So only notify them if the actually running state
         // has changed.
@@ -969,7 +958,7 @@
 
     private void handleFaceAcquired(int acquireInfo) {
         Assert.isMainThread();
-        if (DEBUG_FACE) Log.d(TAG, "Face acquired acquireInfo=" + acquireInfo);
+        mLogger.logFaceAcquired(acquireInfo);
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -982,25 +971,25 @@
         Trace.beginSection("KeyGuardUpdateMonitor#handlerFaceAuthenticated");
         try {
             if (mGoingToSleep) {
-                Log.d(TAG, "Aborted successful auth because device is going to sleep.");
+                mLogger.d("Aborted successful auth because device is going to sleep.");
                 return;
             }
             final int userId;
             try {
                 userId = ActivityManager.getService().getCurrentUser().id;
             } catch (RemoteException e) {
-                Log.e(TAG, "Failed to get current user id: ", e);
+                mLogger.logException(e, "Failed to get current user id");
                 return;
             }
             if (userId != authUserId) {
-                Log.d(TAG, "Face authenticated for wrong user: " + authUserId);
+                mLogger.logFaceAuthForWrongUser(authUserId);
                 return;
             }
             if (isFaceDisabled(userId)) {
-                Log.d(TAG, "Face authentication disabled by DPM for userId: " + userId);
+                mLogger.logFaceAuthDisabledForUser(userId);
                 return;
             }
-            if (DEBUG_FACE) Log.d(TAG, "Face auth succeeded for user " + userId);
+            mLogger.logFaceAuthSuccess(userId);
             onFaceAuthenticated(userId, isStrongBiometric);
         } finally {
             setFaceRunningState(BIOMETRIC_STATE_STOPPED);
@@ -1010,7 +999,7 @@
 
     private void handleFaceHelp(int msgId, String helpString) {
         Assert.isMainThread();
-        if (DEBUG_FACE) Log.d(TAG, "Face help received: " + helpString);
+        mLogger.logFaceAuthHelpMsg(msgId, helpString);
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -1022,15 +1011,21 @@
     private Runnable mRetryFaceAuthentication = new Runnable() {
         @Override
         public void run() {
-            Log.w(TAG, "Retrying face after HW unavailable, attempt " +
-                    mHardwareFaceUnavailableRetryCount);
+            mLogger.logRetryingAfterFaceHwUnavailable(mHardwareFaceUnavailableRetryCount);
             updateFaceListeningState(BIOMETRIC_ACTION_UPDATE);
         }
     };
 
-    private void handleFaceError(int msgId, String errString) {
+    private void onFaceCancelNotReceived() {
+        mLogger.e("Face cancellation not received, transitioning to STOPPED");
+        mFaceRunningState = BIOMETRIC_STATE_STOPPED;
+        KeyguardUpdateMonitor.this.updateFaceListeningState(BIOMETRIC_ACTION_STOP);
+    }
+
+    private void handleFaceError(int msgId, final String originalErrMsg) {
         Assert.isMainThread();
-        if (DEBUG_FACE) Log.d(TAG, "Face error received: " + errString + " msgId=" + msgId);
+        String errString = originalErrMsg;
+        mLogger.logFaceAuthError(msgId, originalErrMsg);
         if (mHandler.hasCallbacks(mFaceCancelNotReceived)) {
             mHandler.removeCallbacks(mFaceCancelNotReceived);
         }
@@ -1087,7 +1082,7 @@
     }
 
     private void handleFaceLockoutReset(@LockoutMode int mode) {
-        Log.d(TAG, "handleFaceLockoutReset: " + mode);
+        mLogger.logFaceLockoutReset(mode);
         final boolean wasLockoutPermanent = mFaceLockedOutPermanent;
         mFaceLockedOutPermanent = (mode == BIOMETRIC_LOCKOUT_PERMANENT);
         final boolean changed = (mFaceLockedOutPermanent != wasLockoutPermanent);
@@ -1105,7 +1100,7 @@
         boolean wasRunning = mFaceRunningState == BIOMETRIC_STATE_RUNNING;
         boolean isRunning = faceRunningState == BIOMETRIC_STATE_RUNNING;
         mFaceRunningState = faceRunningState;
-        Log.d(TAG, "faceRunningState: " + mFaceRunningState);
+        mLogger.logFaceRunningState(mFaceRunningState);
         // Clients of KeyguardUpdateMonitor don't care about the internal state or about the
         // asynchronousness of the cancel cycle. So only notify them if the actually running state
         // has changed.
@@ -1221,8 +1216,7 @@
                     mDevicePolicyManager.getProfileOwnerOrDeviceOwnerSupervisionComponent(
                             UserHandle.of(userId));
             if (supervisorComponent == null) {
-                Log.e(TAG, "No Profile Owner or Device Owner supervision app found for User "
-                        + userId);
+                mLogger.logMissingSupervisorAppError(userId);
             } else {
                 Intent intent =
                         new Intent(DevicePolicyManager.ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE)
@@ -1354,7 +1348,7 @@
         @Override
         public void onReceive(Context context, Intent intent) {
             final String action = intent.getAction();
-            if (DEBUG) Log.d(TAG, "received broadcast " + action);
+            mLogger.logBroadcastReceived(action);
 
             if (Intent.ACTION_TIME_TICK.equals(action)
                     || Intent.ACTION_TIME_CHANGED.equals(action)) {
@@ -1381,12 +1375,10 @@
                     }
                     return;
                 }
-                if (DEBUG_SIM_STATES) {
-                    Log.v(TAG, "action " + action
-                            + " state: " + intent.getStringExtra(
-                            Intent.EXTRA_SIM_STATE)
-                            + " slotId: " + args.slotId + " subid: " + args.subId);
-                }
+                mLogger.logSimStateFromIntent(action,
+                        intent.getStringExtra(Intent.EXTRA_SIM_STATE),
+                        args.slotId,
+                        args.subId);
                 mHandler.obtainMessage(MSG_SIM_STATE_CHANGE, args.subId, args.slotId, args.simState)
                         .sendToTarget();
             } else if (TelephonyManager.ACTION_PHONE_STATE_CHANGED.equals(action)) {
@@ -1398,10 +1390,7 @@
                 ServiceState serviceState = ServiceState.newFromBundle(intent.getExtras());
                 int subId = intent.getIntExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX,
                         SubscriptionManager.INVALID_SUBSCRIPTION_ID);
-                if (DEBUG) {
-                    Log.v(TAG, "action " + action + " serviceState=" + serviceState + " subId="
-                            + subId);
-                }
+                mLogger.logServiceStateIntent(action, serviceState, subId);
                 mHandler.sendMessage(
                         mHandler.obtainMessage(MSG_SERVICE_STATE_CHANGE, subId, 0, serviceState));
             } else if (TelephonyManager.ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED.equals(action)) {
@@ -1521,7 +1510,7 @@
                  */
                 @Override
                 public void onUdfpsPointerDown(int sensorId) {
-                    Log.d(TAG, "onUdfpsPointerDown, sensorId: " + sensorId);
+                    mLogger.logUdfpsPointerDown(sensorId);
                     requestFaceAuth(true);
                 }
 
@@ -1530,7 +1519,7 @@
                  */
                 @Override
                 public void onUdfpsPointerUp(int sensorId) {
-                    Log.d(TAG, "onUdfpsPointerUp, sensorId: " + sensorId);
+                    mLogger.logUdfpsPointerUp(sensorId);
                 }
             };
 
@@ -1826,7 +1815,9 @@
             TelephonyListenerManager telephonyListenerManager,
             InteractionJankMonitor interactionJankMonitor,
             LatencyTracker latencyTracker,
-            ActiveUnlockConfig activeUnlockConfiguration) {
+            ActiveUnlockConfig activeUnlockConfiguration,
+            KeyguardUpdateMonitorLogger logger) {
+        mLogger = logger;
         mContext = context;
         mSubscriptionManager = SubscriptionManager.from(context);
         mTelephonyListenerManager = telephonyListenerManager;
@@ -2170,13 +2161,13 @@
                 || mFingerprintRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING;
         if (runningOrRestarting && !shouldListenForFingerprint) {
             if (action == BIOMETRIC_ACTION_START) {
-                Log.v(TAG, "Ignoring stopListeningForFingerprint()");
+                mLogger.v("Ignoring stopListeningForFingerprint()");
                 return;
             }
             stopListeningForFingerprint();
         } else if (!runningOrRestarting && shouldListenForFingerprint) {
             if (action == BIOMETRIC_ACTION_STOP) {
-                Log.v(TAG, "Ignoring startListeningForFingerprint()");
+                mLogger.v("Ignoring startListeningForFingerprint()");
                 return;
             }
             startListeningForFingerprint();
@@ -2202,7 +2193,7 @@
      * @param active If the interrupt started or ended.
      */
     public void onAuthInterruptDetected(boolean active) {
-        if (DEBUG) Log.d(TAG, "onAuthInterruptDetected(" + active + ")");
+        mLogger.logAuthInterruptDetected(active);
         if (mAuthInterruptActive == active) {
             return;
         }
@@ -2217,7 +2208,7 @@
      * @param userInitiatedRequest true if the user explicitly requested face auth
      */
     public void requestFaceAuth(boolean userInitiatedRequest) {
-        if (DEBUG) Log.d(TAG, "requestFaceAuth() userInitiated=" + userInitiatedRequest);
+        mLogger.logFaceAuthRequested(userInitiatedRequest);
         mIsFaceAuthUserRequested |= userInitiatedRequest;
         updateFaceListeningState(BIOMETRIC_ACTION_START);
     }
@@ -2243,14 +2234,14 @@
         boolean shouldListenForFace = shouldListenForFace();
         if (mFaceRunningState == BIOMETRIC_STATE_RUNNING && !shouldListenForFace) {
             if (action == BIOMETRIC_ACTION_START) {
-                Log.v(TAG, "Ignoring stopListeningForFace()");
+                mLogger.v("Ignoring stopListeningForFace()");
                 return;
             }
             mIsFaceAuthUserRequested = false;
             stopListeningForFace();
         } else if (mFaceRunningState != BIOMETRIC_STATE_RUNNING && shouldListenForFace) {
             if (action == BIOMETRIC_ACTION_STOP) {
-                Log.v(TAG, "Ignoring startListeningForFace()");
+                mLogger.v("Ignoring startListeningForFace()");
                 return;
             }
             startListeningForFace();
@@ -2267,9 +2258,7 @@
         }
 
         if (shouldTriggerActiveUnlock()) {
-            if (DEBUG_ACTIVE_UNLOCK) {
-                Log.d("ActiveUnlock", "initiate active unlock triggerReason=" + reason);
-            }
+            mLogger.logActiveUnlockTriggered(reason);
             mTrustManager.reportUserMayRequestUnlock(KeyguardUpdateMonitor.getCurrentUser());
         }
     }
@@ -2297,12 +2286,7 @@
         }
 
         if (allowRequest && shouldTriggerActiveUnlock()) {
-            if (DEBUG_ACTIVE_UNLOCK) {
-                Log.d("ActiveUnlock", "reportUserRequestedUnlock"
-                        + " origin=" + requestOrigin.name()
-                        + " reason=" + reason
-                        + " dismissKeyguard=" + dismissKeyguard);
-            }
+            mLogger.logUserRequestedUnlock(requestOrigin, reason, dismissKeyguard);
             mTrustManager.reportUserRequestedUnlock(KeyguardUpdateMonitor.getCurrentUser(),
                     dismissKeyguard);
         }
@@ -2452,32 +2436,30 @@
         final boolean shouldListen = shouldListenKeyguardState && shouldListenUserState
                 && shouldListenBouncerState && shouldListenUdfpsState && !isFingerprintLockedOut();
 
-        if (DEBUG_FINGERPRINT || DEBUG_SPEW) {
-            maybeLogListenerModelData(
-                    new KeyguardFingerprintListenModel(
-                        System.currentTimeMillis(),
-                        user,
-                        shouldListen,
-                        biometricEnabledForUser,
-                        mBouncerIsOrWillBeShowing,
-                        userCanSkipBouncer,
-                        mCredentialAttempted,
-                        mDeviceInteractive,
-                        mIsDreaming,
-                        isEncryptedOrLockdownForUser,
-                        fingerprintDisabledForUser,
-                        mFingerprintLockedOut,
-                        mGoingToSleep,
-                        mKeyguardGoingAway,
-                        mKeyguardIsVisible,
-                        mKeyguardOccluded,
-                        mOccludingAppRequestingFp,
-                        mIsPrimaryUser,
-                        shouldListenForFingerprintAssistant,
-                        mSwitchingUser,
-                        isUdfps,
-                        userDoesNotHaveTrust));
-        }
+        maybeLogListenerModelData(
+                new KeyguardFingerprintListenModel(
+                    System.currentTimeMillis(),
+                    user,
+                    shouldListen,
+                    biometricEnabledForUser,
+                    mBouncerIsOrWillBeShowing,
+                    userCanSkipBouncer,
+                    mCredentialAttempted,
+                    mDeviceInteractive,
+                    mIsDreaming,
+                    isEncryptedOrLockdownForUser,
+                    fingerprintDisabledForUser,
+                    mFingerprintLockedOut,
+                    mGoingToSleep,
+                    mKeyguardGoingAway,
+                    mKeyguardIsVisible,
+                    mKeyguardOccluded,
+                    mOccludingAppRequestingFp,
+                    mIsPrimaryUser,
+                    shouldListenForFingerprintAssistant,
+                    mSwitchingUser,
+                    isUdfps,
+                    userDoesNotHaveTrust));
 
         return shouldListen;
     }
@@ -2552,59 +2534,49 @@
                 && !fpLockedout;
 
         // Aggregate relevant fields for debug logging.
-        if (DEBUG_FACE || DEBUG_SPEW) {
-            maybeLogListenerModelData(
-                    new KeyguardFaceListenModel(
-                        System.currentTimeMillis(),
-                        user,
-                        shouldListen,
-                        mAuthInterruptActive,
-                        becauseCannotSkipBouncer,
-                        biometricEnabledForUser,
-                        mBouncerFullyShown,
-                        faceAuthenticated,
-                        faceDisabledForUser,
-                        mGoingToSleep,
-                        awakeKeyguard,
-                        mKeyguardGoingAway,
-                        shouldListenForFaceAssistant,
-                        mOccludingAppRequestingFace,
-                        mIsPrimaryUser,
-                        strongAuthAllowsScanning,
-                        mSecureCameraLaunched,
-                        mSwitchingUser,
-                        mUdfpsBouncerShowing));
-        }
+        maybeLogListenerModelData(
+                new KeyguardFaceListenModel(
+                    System.currentTimeMillis(),
+                    user,
+                    shouldListen,
+                    mAuthInterruptActive,
+                    becauseCannotSkipBouncer,
+                    biometricEnabledForUser,
+                    mBouncerFullyShown,
+                    faceAuthenticated,
+                    faceDisabledForUser,
+                    mGoingToSleep,
+                    awakeKeyguard,
+                    mKeyguardGoingAway,
+                    shouldListenForFaceAssistant,
+                    mOccludingAppRequestingFace,
+                    mIsPrimaryUser,
+                    strongAuthAllowsScanning,
+                    mSecureCameraLaunched,
+                    mSwitchingUser,
+                    mUdfpsBouncerShowing));
 
         return shouldListen;
     }
 
     private void maybeLogListenerModelData(KeyguardListenModel model) {
-        // Too chatty, but very useful when debugging issues.
-        if (DEBUG_SPEW) {
-            Log.v(TAG, model.toString());
-        }
+        mLogger.logKeyguardListenerModel(model);
 
-        if (DEBUG_ACTIVE_UNLOCK
-                && model instanceof KeyguardActiveUnlockModel) {
+        if (model instanceof KeyguardActiveUnlockModel) {
             mListenModels.add(model);
             return;
         }
 
         // Add model data to the historical buffer.
         final boolean notYetRunning =
-                (DEBUG_FACE
-                    && model instanceof KeyguardFaceListenModel
-                    && mFaceRunningState != BIOMETRIC_STATE_RUNNING)
-                || (DEBUG_FINGERPRINT
-                    && model instanceof KeyguardFingerprintListenModel
-                    && mFingerprintRunningState != BIOMETRIC_STATE_RUNNING);
+                (model instanceof KeyguardFaceListenModel
+                        && mFaceRunningState != BIOMETRIC_STATE_RUNNING)
+                || (model instanceof KeyguardFingerprintListenModel
+                        && mFingerprintRunningState != BIOMETRIC_STATE_RUNNING);
         final boolean running =
-                (DEBUG_FACE
-                        && model instanceof KeyguardFaceListenModel
+                (model instanceof KeyguardFaceListenModel
                         && mFaceRunningState == BIOMETRIC_STATE_RUNNING)
-                        || (DEBUG_FINGERPRINT
-                        && model instanceof KeyguardFingerprintListenModel
+                        || (model instanceof KeyguardFingerprintListenModel
                         && mFingerprintRunningState == BIOMETRIC_STATE_RUNNING);
         if (notYetRunning && model.getListening()
                 || running && !model.getListening()) {
@@ -2616,9 +2588,9 @@
         final int userId = getCurrentUser();
         final boolean unlockPossible = isUnlockWithFingerprintPossible(userId);
         if (mFingerprintCancelSignal != null) {
-            Log.e(TAG, "Cancellation signal is not null, high chance of bug in fp auth lifecycle"
-                    + " management. FP state: " + mFingerprintRunningState
-                    + ", unlockPossible: " + unlockPossible);
+            mLogger.logUnexpectedFpCancellationSignalState(
+                    mFingerprintRunningState,
+                    unlockPossible);
         }
 
         if (mFingerprintRunningState == BIOMETRIC_STATE_CANCELLING) {
@@ -2629,7 +2601,7 @@
             // Waiting for restart via handleFingerprintError().
             return;
         }
-        if (DEBUG) Log.v(TAG, "startListeningForFingerprint()");
+        mLogger.v("startListeningForFingerprint()");
 
         if (unlockPossible) {
             mFingerprintCancelSignal = new CancellationSignal();
@@ -2650,9 +2622,7 @@
         final int userId = getCurrentUser();
         final boolean unlockPossible = isUnlockWithFacePossible(userId);
         if (mFaceCancelSignal != null) {
-            Log.e(TAG, "Cancellation signal is not null, high chance of bug in face auth lifecycle"
-                    + " management. Face state: " + mFaceRunningState
-                    + ", unlockPossible: " + unlockPossible);
+            mLogger.logUnexpectedFaceCancellationSignalState(mFaceRunningState, unlockPossible);
         }
 
         if (mFaceRunningState == BIOMETRIC_STATE_CANCELLING) {
@@ -2662,7 +2632,7 @@
             // Waiting for ERROR_CANCELED before requesting auth again
             return;
         }
-        if (DEBUG) Log.v(TAG, "startListeningForFace(): " + mFaceRunningState);
+        mLogger.logStartedListeningForFace(mFaceRunningState);
 
         if (unlockPossible) {
             mFaceCancelSignal = new CancellationSignal();
@@ -2728,7 +2698,7 @@
     }
 
     private void stopListeningForFingerprint() {
-        if (DEBUG) Log.v(TAG, "stopListeningForFingerprint()");
+        mLogger.v("stopListeningForFingerprint()");
         if (mFingerprintRunningState == BIOMETRIC_STATE_RUNNING) {
             if (mFingerprintCancelSignal != null) {
                 mFingerprintCancelSignal.cancel();
@@ -2744,7 +2714,7 @@
     }
 
     private void stopListeningForFace() {
-        if (DEBUG) Log.v(TAG, "stopListeningForFace()");
+        mLogger.v("stopListeningForFace()");
         if (mFaceRunningState == BIOMETRIC_STATE_RUNNING) {
             if (mFaceCancelSignal != null) {
                 mFaceCancelSignal.cancel();
@@ -2773,7 +2743,7 @@
                 if (mDeviceProvisioned) {
                     mHandler.sendEmptyMessage(MSG_DEVICE_PROVISIONED);
                 }
-                if (DEBUG) Log.d(TAG, "DEVICE_PROVISIONED state = " + mDeviceProvisioned);
+                mLogger.logDeviceProvisionedState(mDeviceProvisioned);
             }
         };
 
@@ -2878,7 +2848,7 @@
      */
     private void handlePhoneStateChanged(String newState) {
         Assert.isMainThread();
-        if (DEBUG) Log.d(TAG, "handlePhoneStateChanged(" + newState + ")");
+        mLogger.logPhoneStateChanged(newState);
         if (TelephonyManager.EXTRA_STATE_IDLE.equals(newState)) {
             mPhoneState = TelephonyManager.CALL_STATE_IDLE;
         } else if (TelephonyManager.EXTRA_STATE_OFFHOOK.equals(newState)) {
@@ -2899,7 +2869,7 @@
      */
     private void handleTimeUpdate() {
         Assert.isMainThread();
-        if (DEBUG) Log.d(TAG, "handleTimeUpdate");
+        mLogger.d("handleTimeUpdate");
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -2913,7 +2883,7 @@
      */
     private void handleTimeZoneUpdate(String timeZone) {
         Assert.isMainThread();
-        if (DEBUG) Log.d(TAG, "handleTimeZoneUpdate");
+        mLogger.d("handleTimeZoneUpdate");
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -2931,7 +2901,7 @@
      */
     private void handleTimeFormatUpdate(String timeFormat) {
         Assert.isMainThread();
-        if (DEBUG) Log.d(TAG, "handleTimeFormatUpdate timeFormat=" + timeFormat);
+        mLogger.logTimeFormatChanged(timeFormat);
         for (int i = 0; i < mCallbacks.size(); i++) {
             KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
             if (cb != null) {
@@ -2945,7 +2915,7 @@
      */
     private void handleBatteryUpdate(BatteryStatus status) {
         Assert.isMainThread();
-        if (DEBUG) Log.d(TAG, "handleBatteryUpdate");
+        mLogger.d("handleBatteryUpdate");
         final boolean batteryUpdateInteresting = isBatteryUpdateInteresting(mBatteryStatus, status);
         mBatteryStatus = status;
         if (batteryUpdateInteresting) {
@@ -2982,14 +2952,11 @@
     @VisibleForTesting
     void handleSimStateChange(int subId, int slotId, int state) {
         Assert.isMainThread();
-        if (DEBUG_SIM_STATES) {
-            Log.d(TAG, "handleSimStateChange(subId=" + subId + ", slotId="
-                    + slotId + ", state=" + state + ")");
-        }
+        mLogger.logSimState(subId, slotId, state);
 
         boolean becameAbsent = false;
         if (!SubscriptionManager.isValidSubscriptionId(subId)) {
-            Log.w(TAG, "invalid subId in handleSimStateChange()");
+            mLogger.w("invalid subId in handleSimStateChange()");
             /* Only handle No SIM(ABSENT) and Card Error(CARD_IO_ERROR) due to
              * handleServiceStateChange() handle other case */
             if (state == TelephonyManager.SIM_STATE_ABSENT) {
@@ -3038,13 +3005,10 @@
      */
     @VisibleForTesting
     void handleServiceStateChange(int subId, ServiceState serviceState) {
-        if (DEBUG) {
-            Log.d(TAG,
-                    "handleServiceStateChange(subId=" + subId + ", serviceState=" + serviceState);
-        }
+        mLogger.logServiceStateChange(subId, serviceState);
 
         if (!SubscriptionManager.isValidSubscriptionId(subId)) {
-            Log.w(TAG, "invalid subId in handleServiceStateChange()");
+            mLogger.w("invalid subId in handleServiceStateChange()");
             return;
         } else {
             updateTelephonyCapable(true);
@@ -3066,7 +3030,7 @@
      */
     public void onKeyguardVisibilityChanged(boolean showing) {
         Assert.isMainThread();
-        Log.d(TAG, "onKeyguardVisibilityChanged(" + showing + ")");
+        mLogger.logKeyguardVisibilityChanged(showing);
         mKeyguardIsVisible = showing;
 
         if (showing) {
@@ -3086,7 +3050,7 @@
      * Handle {@link #MSG_KEYGUARD_RESET}
      */
     private void handleKeyguardReset() {
-        if (DEBUG) Log.d(TAG, "handleKeyguardReset");
+        mLogger.d("handleKeyguardReset");
         updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE);
         mNeedsSlowUnlockTransition = resolveNeedsSlowUnlockTransition();
     }
@@ -3100,8 +3064,8 @@
                 0 /* flags */, getCurrentUser());
 
         if (resolveInfo == null) {
-            Log.w(TAG, "resolveNeedsSlowUnlockTransition: returning false since activity "
-                    + "could not be resolved.");
+            mLogger.w("resolveNeedsSlowUnlockTransition: returning false since activity could "
+                            + "not be resolved.");
             return false;
         }
 
@@ -3119,11 +3083,7 @@
         final boolean wasBouncerFullyShown = mBouncerFullyShown;
         mBouncerIsOrWillBeShowing = bouncerIsOrWillBeShowing == 1;
         mBouncerFullyShown = bouncerFullyShown == 1;
-        if (DEBUG) {
-            Log.d(TAG, "handleKeyguardBouncerChanged"
-                    + " bouncerIsOrWillBeShowing=" + mBouncerIsOrWillBeShowing
-                    + " bouncerFullyShowing=" + mBouncerFullyShown);
-        }
+        mLogger.logKeyguardBouncerChanged(mBouncerIsOrWillBeShowing, mBouncerFullyShown);
 
         if (mBouncerFullyShown) {
             // If the bouncer is shown, always clear this flag. This can happen in the following
@@ -3243,9 +3203,7 @@
      */
     public void removeCallback(KeyguardUpdateMonitorCallback callback) {
         Assert.isMainThread();
-        if (DEBUG) {
-            Log.v(TAG, "*** unregister callback for " + callback);
-        }
+        mLogger.logUnregisterCallback(callback);
 
         mCallbacks.removeIf(el -> el.get() == callback);
     }
@@ -3258,15 +3216,14 @@
      */
     public void registerCallback(KeyguardUpdateMonitorCallback callback) {
         Assert.isMainThread();
-        if (DEBUG) Log.v(TAG, "*** register callback for " + callback);
+        mLogger.logRegisterCallback(callback);
         // Prevent adding duplicate callbacks
 
         for (int i = 0; i < mCallbacks.size(); i++) {
             if (mCallbacks.get(i).get() == callback) {
-                if (DEBUG) {
-                    Log.e(TAG, "Object tried to add another callback",
-                            new Exception("Called by"));
-                }
+                mLogger.logException(
+                        new Exception("Called by"),
+                        "Object tried to add another callback");
                 return;
             }
         }
@@ -3316,11 +3273,7 @@
      */
     public void sendKeyguardBouncerChanged(boolean bouncerIsOrWillBeShowing,
             boolean bouncerFullyShown) {
-        if (DEBUG) {
-            Log.d(TAG, "sendKeyguardBouncerChanged"
-                    + " bouncerIsOrWillBeShowing=" + bouncerIsOrWillBeShowing
-                    + " bouncerFullyShown=" + bouncerFullyShown);
-        }
+        mLogger.logSendKeyguardBouncerChanged(bouncerIsOrWillBeShowing, bouncerFullyShown);
         Message message = mHandler.obtainMessage(MSG_KEYGUARD_BOUNCER_CHANGED);
         message.arg1 = bouncerIsOrWillBeShowing ? 1 : 0;
         message.arg2 = bouncerFullyShown ? 1 : 0;
@@ -3337,7 +3290,7 @@
      */
     @MainThread
     public void reportSimUnlocked(int subId) {
-        if (DEBUG_SIM_STATES) Log.v(TAG, "reportSimUnlocked(subId=" + subId + ")");
+        mLogger.logSimUnlocked(subId);
         handleSimStateChange(subId, getSlotId(subId), TelephonyManager.SIM_STATE_READY);
     }
 
@@ -3614,7 +3567,9 @@
         try {
             ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver);
         } catch (RemoteException e) {
-            Log.d(TAG, "RemoteException onDestroy. cannot unregister userSwitchObserver");
+            mLogger.logException(
+                    e,
+                    "RemoteException onDestroy. cannot unregister userSwitchObserver");
         }
 
         TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener);
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
new file mode 100644
index 0000000..035b7f0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardUpdateMonitorLogger.kt
@@ -0,0 +1,326 @@
+/*
+ * 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.logging
+
+import android.hardware.biometrics.BiometricConstants.LockoutMode
+import android.telephony.ServiceState
+import android.telephony.SubscriptionInfo
+import com.android.keyguard.ActiveUnlockConfig
+import com.android.keyguard.KeyguardListenModel
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.LogLevel.DEBUG
+import com.android.systemui.log.LogLevel.ERROR
+import com.android.systemui.log.LogLevel.INFO
+import com.android.systemui.log.LogLevel.VERBOSE
+import com.android.systemui.log.LogLevel.WARNING
+import com.android.systemui.log.dagger.KeyguardUpdateMonitorLog
+import com.google.errorprone.annotations.CompileTimeConstant
+import javax.inject.Inject
+
+private const val TAG = "KeyguardUpdateMonitorLog"
+
+/**
+ * Helper class for logging for [com.android.keyguard.KeyguardUpdateMonitor]
+ */
+class KeyguardUpdateMonitorLogger @Inject constructor(
+        @KeyguardUpdateMonitorLog private val logBuffer: LogBuffer
+) {
+    fun d(@CompileTimeConstant msg: String) = log(msg, DEBUG)
+
+    fun e(@CompileTimeConstant msg: String) = log(msg, ERROR)
+
+    fun v(@CompileTimeConstant msg: String) = log(msg, ERROR)
+
+    fun w(@CompileTimeConstant msg: String) = log(msg, WARNING)
+
+    fun log(@CompileTimeConstant msg: String, level: LogLevel) = logBuffer.log(TAG, level, msg)
+
+    fun logActiveUnlockTriggered(reason: String) {
+        logBuffer.log("ActiveUnlock", DEBUG,
+                { str1 = reason },
+                { "initiate active unlock triggerReason=$str1" })
+    }
+
+    fun logAuthInterruptDetected(active: Boolean) {
+        logBuffer.log(TAG, DEBUG,
+                { bool1 = active },
+                { "onAuthInterruptDetected($bool1)" })
+    }
+
+    fun logBroadcastReceived(action: String?) {
+        logBuffer.log(TAG, DEBUG, { str1 = action }, { "received broadcast $str1" })
+    }
+
+    fun logDeviceProvisionedState(deviceProvisioned: Boolean) {
+        logBuffer.log(TAG, DEBUG,
+                { bool1 = deviceProvisioned },
+                { "DEVICE_PROVISIONED state = $bool1" })
+    }
+
+    fun logException(ex: Exception, @CompileTimeConstant logMsg: String) {
+        logBuffer.log(TAG, ERROR, {}, { logMsg }, exception = ex)
+    }
+
+    fun logFaceAcquired(acquireInfo: Int) {
+        logBuffer.log(TAG, DEBUG,
+                { int1 = acquireInfo },
+                { "Face acquired acquireInfo=$int1" })
+    }
+
+    fun logFaceAuthDisabledForUser(userId: Int) {
+        logBuffer.log(TAG, DEBUG,
+                { int1 = userId },
+                { "Face authentication disabled by DPM for userId: $int1" })
+    }
+    fun logFaceAuthError(msgId: Int, originalErrMsg: String) {
+        logBuffer.log(TAG, DEBUG, {
+                    str1 = originalErrMsg
+                    int1 = msgId
+                }, { "Face error received: $str1 msgId= $int1" })
+    }
+
+    fun logFaceAuthForWrongUser(authUserId: Int) {
+        logBuffer.log(TAG, DEBUG,
+                { int1 = authUserId },
+                { "Face authenticated for wrong user: $int1" })
+    }
+
+    fun logFaceAuthHelpMsg(msgId: Int, helpMsg: String) {
+        logBuffer.log(TAG, DEBUG, {
+                    int1 = msgId
+                    str1 = helpMsg
+                }, { "Face help received, msgId: $int1 msg: $str1" })
+    }
+
+    fun logFaceAuthRequested(userInitiatedRequest: Boolean) {
+        logBuffer.log(TAG, DEBUG,
+                { bool1 = userInitiatedRequest },
+                { "requestFaceAuth() userInitiated=$bool1" })
+    }
+
+    fun logFaceAuthSuccess(userId: Int) {
+        logBuffer.log(TAG, DEBUG,
+                { int1 = userId },
+                { "Face auth succeeded for user $int1" })
+    }
+
+    fun logFaceLockoutReset(@LockoutMode mode: Int) {
+        logBuffer.log(TAG, DEBUG, { int1 = mode }, { "handleFaceLockoutReset: $int1" })
+    }
+
+    fun logFaceRunningState(faceRunningState: Int) {
+        logBuffer.log(TAG, DEBUG, { int1 = faceRunningState }, { "faceRunningState: $int1" })
+    }
+
+    fun logFingerprintAuthForWrongUser(authUserId: Int) {
+        logBuffer.log(TAG, DEBUG,
+                { int1 = authUserId },
+                { "Fingerprint authenticated for wrong user: $int1" })
+    }
+
+    fun logFingerprintDisabledForUser(userId: Int) {
+        logBuffer.log(TAG, DEBUG,
+                { int1 = userId },
+                { "Fingerprint disabled by DPM for userId: $int1" })
+    }
+
+    fun logFingerprintLockoutReset(@LockoutMode mode: Int) {
+        logBuffer.log(TAG, DEBUG, { int1 = mode }, { "handleFingerprintLockoutReset: $int1" })
+    }
+
+    fun logFingerprintRunningState(fingerprintRunningState: Int) {
+        logBuffer.log(TAG, DEBUG,
+                { int1 = fingerprintRunningState },
+                { "fingerprintRunningState: $int1" })
+    }
+
+    fun logInvalidSubId(subId: Int) {
+        logBuffer.log(TAG, INFO,
+                { int1 = subId },
+                { "Previously active sub id $int1 is now invalid, will remove" })
+    }
+
+    fun logKeyguardBouncerChanged(bouncerIsOrWillBeShowing: Boolean, bouncerFullyShown: Boolean) {
+        logBuffer.log(TAG, DEBUG, {
+            bool1 = bouncerIsOrWillBeShowing
+            bool2 = bouncerFullyShown
+        }, {
+            "handleKeyguardBouncerChanged " +
+                    "bouncerIsOrWillBeShowing=$bool1 bouncerFullyShowing=$bool2"
+        })
+    }
+
+    fun logKeyguardListenerModel(model: KeyguardListenModel) {
+        logBuffer.log(TAG, VERBOSE, { str1 = "$model" }, { str1!! })
+    }
+
+    fun logKeyguardVisibilityChanged(showing: Boolean) {
+        logBuffer.log(TAG, DEBUG, { bool1 = showing }, { "onKeyguardVisibilityChanged($bool1)" })
+    }
+
+    fun logMissingSupervisorAppError(userId: Int) {
+        logBuffer.log(TAG, ERROR,
+                { int1 = userId },
+                { "No Profile Owner or Device Owner supervision app found for User $int1" })
+    }
+
+    fun logPhoneStateChanged(newState: String) {
+        logBuffer.log(TAG, DEBUG,
+                { str1 = newState },
+                { "handlePhoneStateChanged($str1)" })
+    }
+
+    fun logRegisterCallback(callback: KeyguardUpdateMonitorCallback?) {
+        logBuffer.log(TAG, VERBOSE,
+                { str1 = "$callback" },
+                { "*** register callback for $str1" })
+    }
+
+    fun logRetryingAfterFaceHwUnavailable(retryCount: Int) {
+        logBuffer.log(TAG, WARNING,
+                { int1 = retryCount },
+                { "Retrying face after HW unavailable, attempt $int1" })
+    }
+
+    fun logRetryAfterFpHwUnavailable(retryCount: Int) {
+        logBuffer.log(TAG, WARNING,
+                { int1 = retryCount },
+                { "Retrying fingerprint attempt: $int1" })
+    }
+
+    fun logSendKeyguardBouncerChanged(
+        bouncerIsOrWillBeShowing: Boolean,
+        bouncerFullyShown: Boolean,
+    ) {
+        logBuffer.log(TAG, DEBUG, {
+            bool1 = bouncerIsOrWillBeShowing
+            bool2 = bouncerFullyShown
+        }, {
+            "sendKeyguardBouncerChanged bouncerIsOrWillBeShowing=$bool1 " +
+                    "bouncerFullyShown=$bool2"
+        })
+    }
+
+    fun logServiceStateChange(subId: Int, serviceState: ServiceState?) {
+        logBuffer.log(TAG, DEBUG, {
+            int1 = subId
+            str1 = "$serviceState"
+        }, { "handleServiceStateChange(subId=$int1, serviceState=$str1)" })
+    }
+
+    fun logServiceStateIntent(action: String, serviceState: ServiceState?, subId: Int) {
+        logBuffer.log(TAG, VERBOSE, {
+            str1 = action
+            str2 = "$serviceState"
+            int1 = subId
+        }, { "action $str1 serviceState=$str2 subId=$int1" })
+    }
+
+    fun logSimState(subId: Int, slotId: Int, state: Int) {
+        logBuffer.log(TAG, DEBUG, {
+            int1 = subId
+            int2 = slotId
+            long1 = state.toLong()
+        }, { "handleSimStateChange(subId=$int1, slotId=$int2, state=$long1)" })
+    }
+
+    fun logSimStateFromIntent(action: String, extraSimState: String, slotId: Int, subId: Int) {
+        logBuffer.log(TAG, VERBOSE, {
+            str1 = action
+            str2 = extraSimState
+            int1 = slotId
+            int2 = subId
+        }, { "action $str1 state: $str2 slotId: $int1 subid: $int2" })
+    }
+
+    fun logSimUnlocked(subId: Int) {
+        logBuffer.log(TAG, VERBOSE, { int1 = subId }, { "reportSimUnlocked(subId=$int1)" })
+    }
+
+    fun logStartedListeningForFace(faceRunningState: Int) {
+        logBuffer.log(TAG, VERBOSE,
+                { int1 = faceRunningState },
+                { "startListeningForFace(): $int1" })
+    }
+
+    fun logSubInfo(subInfo: SubscriptionInfo?) {
+        logBuffer.log(TAG, VERBOSE,
+                { str1 = "$subInfo" },
+                { "SubInfo:$str1" })
+    }
+
+    fun logTimeFormatChanged(newTimeFormat: String) {
+        logBuffer.log(TAG, DEBUG,
+                { str1 = newTimeFormat },
+                { "handleTimeFormatUpdate timeFormat=$str1" })
+    }
+
+    fun logUdfpsPointerDown(sensorId: Int) {
+        logBuffer.log(TAG, DEBUG,
+                { int1 = sensorId },
+                { "onUdfpsPointerDown, sensorId: $int1" })
+    }
+    fun logUdfpsPointerUp(sensorId: Int) {
+        logBuffer.log(TAG, DEBUG,
+                { int1 = sensorId },
+                { "onUdfpsPointerUp, sensorId: $int1" })
+    }
+
+    fun logUnexpectedFaceCancellationSignalState(faceRunningState: Int, unlockPossible: Boolean) {
+        logBuffer.log(TAG, ERROR, {
+                    int1 = faceRunningState
+                    bool1 = unlockPossible
+                }, {
+                    "Cancellation signal is not null, high chance of bug in " +
+                            "face auth lifecycle management. " +
+                            "Face state: $int1, unlockPossible: $bool1"
+                })
+    }
+
+    fun logUnexpectedFpCancellationSignalState(
+        fingerprintRunningState: Int,
+        unlockPossible: Boolean
+    ) {
+        logBuffer.log(TAG, ERROR, {
+                    int1 = fingerprintRunningState
+                    bool1 = unlockPossible
+                }, {
+                    "Cancellation signal is not null, high chance of bug in " +
+                            "fp auth lifecycle management. FP state: $int1, unlockPossible: $bool1"
+                })
+    }
+
+    fun logUnregisterCallback(callback: KeyguardUpdateMonitorCallback?) {
+        logBuffer.log(TAG, VERBOSE,
+                { str1 = "$callback" },
+                { "*** unregister callback for $str1" })
+    }
+
+    fun logUserRequestedUnlock(
+        requestOrigin: ActiveUnlockConfig.ACTIVE_UNLOCK_REQUEST_ORIGIN,
+        reason: String,
+        dismissKeyguard: Boolean
+    ) {
+        logBuffer.log("ActiveUnlock", DEBUG, {
+                    str1 = requestOrigin.name
+                    str2 = reason
+                    bool1 = dismissKeyguard
+                }, { "reportUserRequestedUnlock origin=$str1 reason=$str2 dismissKeyguard=$bool1" })
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 720d708..5c84ff3 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -81,7 +81,6 @@
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.statusbar.events.PrivacyDotViewController;
@@ -323,7 +322,6 @@
     @Inject Lazy<SmartReplyConstants> mSmartReplyConstants;
     @Inject Lazy<NotificationListener> mNotificationListener;
     @Inject Lazy<NotificationLogger> mNotificationLogger;
-    @Inject Lazy<NotificationViewHierarchyManager> mNotificationViewHierarchyManager;
     @Inject Lazy<NotificationFilter> mNotificationFilter;
     @Inject Lazy<KeyguardDismissUtil> mKeyguardDismissUtil;
     @Inject Lazy<SmartReplyController> mSmartReplyController;
@@ -540,8 +538,6 @@
         mProviders.put(SmartReplyConstants.class, mSmartReplyConstants::get);
         mProviders.put(NotificationListener.class, mNotificationListener::get);
         mProviders.put(NotificationLogger.class, mNotificationLogger::get);
-        mProviders.put(NotificationViewHierarchyManager.class,
-                mNotificationViewHierarchyManager::get);
         mProviders.put(NotificationFilter.class, mNotificationFilter::get);
         mProviders.put(KeyguardDismissUtil.class, mKeyguardDismissUtil::get);
         mProviders.put(SmartReplyController.class, mSmartReplyController::get);
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
index e9ca0fd..5f586c9 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIInitializer.java
@@ -79,11 +79,6 @@
 
         // Stand up WMComponent
         setupWmComponent(mContext);
-        if (initializeComponents) {
-            // Only initialize when not starting from tests since this currently initializes some
-            // components that shouldn't be run in the test environment
-            mWMComponent.init();
-        }
 
         // And finally, retrieve whatever SysUI needs from WMShell and build SysUI.
         SysUIComponent.Builder builder = mRootComponent.getSysUIComponent();
@@ -102,6 +97,10 @@
                     .setDisplayAreaHelper(mWMComponent.getDisplayAreaHelper())
                     .setRecentTasks(mWMComponent.getRecentTasks())
                     .setBackAnimation(mWMComponent.getBackAnimation());
+
+            // Only initialize when not starting from tests since this currently initializes some
+            // components that shouldn't be run in the test environment
+            mWMComponent.init();
         } else {
             // TODO: Call on prepareSysUIComponentBuilder but not with real components. Other option
             // is separating this logic into newly creating SystemUITestsFactory.
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
index 448b99b..a1288b5 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/SystemActions.java
@@ -500,7 +500,7 @@
 
     private void handleTakeScreenshot() {
         ScreenshotHelper screenshotHelper = new ScreenshotHelper(mContext);
-        screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN, true, true,
+        screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
                 SCREENSHOT_ACCESSIBILITY_ACTIONS, new Handler(Looper.getMainLooper()), null);
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
index bbffb73..d03106b 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/SidefpsController.kt
@@ -43,6 +43,8 @@
 import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
 import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
 import android.view.accessibility.AccessibilityEvent
 import androidx.annotation.RawRes
 import com.airbnb.lottie.LottieAnimationView
@@ -130,7 +132,7 @@
         fitInsetsTypes = 0 // overrides default, avoiding status bars during layout
         gravity = Gravity.TOP or Gravity.LEFT
         layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
-        privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
+        privateFlags = PRIVATE_FLAG_TRUSTED_OVERLAY or PRIVATE_FLAG_NO_MOVE_ANIMATION
     }
 
     init {
diff --git a/packages/SystemUI/src/com/android/systemui/common/coroutine/ChannelExt.kt b/packages/SystemUI/src/com/android/systemui/common/coroutine/ChannelExt.kt
new file mode 100644
index 0000000..6f3beac
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/coroutine/ChannelExt.kt
@@ -0,0 +1,54 @@
+/*
+ *  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.common.coroutine
+
+import android.util.Log
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.channels.onFailure
+
+object ChannelExt {
+
+    /**
+     * Convenience wrapper around [SendChannel.trySend] that also logs on failure. This is the
+     * equivalent of calling:
+     *
+     * ```
+     * sendChannel.trySend(element).onFailure {
+     *     Log.e(
+     *         loggingTag,
+     *         "Failed to send $elementDescription" +
+     *             " - downstream canceled or failed.",
+     *          it,
+     *    )
+     *}
+     * ```
+     */
+    fun <T> SendChannel<T>.trySendWithFailureLogging(
+        element: T,
+        loggingTag: String,
+        elementDescription: String = "updated state",
+    ) {
+        trySend(element).onFailure {
+            Log.e(
+                loggingTag,
+                "Failed to send $elementDescription - downstream canceled or failed.",
+                it,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt b/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt
new file mode 100644
index 0000000..d4a1f74
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/coroutine/ConflatedCallbackFlow.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.common.coroutine
+
+import kotlin.experimental.ExperimentalTypeInference
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ProducerScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.buffer
+import kotlinx.coroutines.flow.callbackFlow
+
+object ConflatedCallbackFlow {
+
+    /**
+     * A [callbackFlow] that uses a buffer [Channel] that is "conflated" meaning that, if
+     * backpressure occurs (if the producer that emits new values into the flow is faster than the
+     * consumer(s) of the values in the flow), the values are buffered and, if the buffer fills up,
+     * we drop the oldest values automatically instead of suspending the producer.
+     */
+    @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
+    @OptIn(ExperimentalTypeInference::class, ExperimentalCoroutinesApi::class)
+    fun <T> conflatedCallbackFlow(
+        @BuilderInference block: suspend ProducerScope<T>.() -> Unit,
+    ): Flow<T> = callbackFlow(block).buffer(capacity = Channel.CONFLATED)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/common/data/model/Position.kt b/packages/SystemUI/src/com/android/systemui/common/data/model/Position.kt
new file mode 100644
index 0000000..7c9df10
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/data/model/Position.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.common.data.model
+
+/** Models a two-dimensional position */
+data class Position(
+    val x: Int,
+    val y: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/common/domain/model/Position.kt b/packages/SystemUI/src/com/android/systemui/common/domain/model/Position.kt
new file mode 100644
index 0000000..f697c0a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/common/domain/model/Position.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.common.domain.model
+
+import com.android.systemui.common.data.model.Position as DataLayerPosition
+
+/** Models a two-dimensional position */
+data class Position(
+    val x: Int,
+    val y: Int,
+) {
+    companion object {
+        fun DataLayerPosition.toDomainLayer(): Position {
+            return Position(
+                x = x,
+                y = y,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt b/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt
new file mode 100644
index 0000000..d6a059d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/containeddrawable/ContainedDrawable.kt
@@ -0,0 +1,27 @@
+/*
+ *  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.containeddrawable
+
+import android.graphics.drawable.Drawable
+import androidx.annotation.DrawableRes
+
+/** Convenience container for [Drawable] or a way to load it later. */
+sealed class ContainedDrawable {
+    data class WithDrawable(val drawable: Drawable) : ContainedDrawable()
+    data class WithResource(@DrawableRes val resourceId: Int) : ContainedDrawable()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
index 2fd3731..9e4a364 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
@@ -21,20 +21,22 @@
 import android.database.ContentObserver
 import android.os.UserHandle
 import android.provider.Settings
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
 import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.controller.ControlsTileResourceConfiguration
+import com.android.systemui.controls.controller.ControlsTileResourceConfigurationImpl
 import com.android.systemui.controls.management.ControlsListingController
 import com.android.systemui.controls.ui.ControlsUiController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.settings.SecureSettings
-import com.android.internal.widget.LockPatternUtils
-import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
-import com.android.systemui.controls.controller.ControlsTileResourceConfiguration
-import com.android.systemui.controls.controller.ControlsTileResourceConfigurationImpl
 import dagger.Lazy
 import java.util.Optional
 import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
 
 /**
  * Pseudo-component to inject into classes outside `com.android.systemui.controls`.
@@ -59,7 +61,8 @@
     private val contentResolver: ContentResolver
         get() = context.contentResolver
 
-    private var canShowWhileLockedSetting = false
+    private val _canShowWhileLockedSetting = MutableStateFlow(false)
+    val canShowWhileLockedSetting = _canShowWhileLockedSetting.asStateFlow()
 
     private val controlsTileResourceConfiguration: ControlsTileResourceConfiguration =
         optionalControlsTileResourceConfiguration.orElse(
@@ -117,7 +120,7 @@
                 == STRONG_AUTH_REQUIRED_AFTER_BOOT) {
             return Visibility.AVAILABLE_AFTER_UNLOCK
         }
-        if (!canShowWhileLockedSetting && !keyguardStateController.isUnlocked()) {
+        if (!canShowWhileLockedSetting.value && !keyguardStateController.isUnlocked()) {
             return Visibility.AVAILABLE_AFTER_UNLOCK
         }
 
@@ -125,7 +128,7 @@
     }
 
     private fun updateShowWhileLocked() {
-        canShowWhileLockedSetting = secureSettings.getIntForUser(
+        _canShowWhileLockedSetting.value = secureSettings.getIntForUser(
             Settings.Secure.LOCKSCREEN_SHOW_CONTROLS, 0, UserHandle.USER_CURRENT) != 0
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
index 718befa..029cabb 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SysUIComponent.java
@@ -31,7 +31,6 @@
 import com.android.systemui.media.taptotransfer.receiver.MediaTttChipControllerReceiver;
 import com.android.systemui.media.taptotransfer.sender.MediaTttChipControllerSender;
 import com.android.systemui.people.PeopleProvider;
-import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.unfold.FoldStateLogger;
 import com.android.systemui.unfold.FoldStateLoggingProvider;
@@ -131,7 +130,6 @@
         getMediaTttCommandLineHelper();
         getMediaMuteAwaitConnectionCli();
         getNearbyMediaDevicesManager();
-        getConnectivityInfoProcessor();
         getUnfoldLatencyTracker().init();
         getFoldStateLoggingProvider().ifPresent(FoldStateLoggingProvider::init);
         getFoldStateLogger().ifPresent(FoldStateLogger::init);
@@ -214,9 +212,6 @@
     /** */
     Optional<NearbyMediaDevicesManager> getNearbyMediaDevicesManager();
 
-    /** */
-    Optional<ConnectivityInfoProcessor> getConnectivityInfoProcessor();
-
     /**
      * Returns {@link CoreStartable}s that should be started with the application.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt
new file mode 100644
index 0000000..d853e04
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/doze/util/BurnInHelperWrapper.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2016 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.doze.util
+
+import javax.inject.Inject
+
+/** Injectable wrapper around `BurnInHelper` functions */
+class BurnInHelperWrapper @Inject constructor() {
+
+    fun burnInOffset(amplitude: Int, xAxis: Boolean): Int {
+        return getBurnInOffset(amplitude, xAxis)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
index 51bd311..5457144 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/Complication.java
@@ -162,7 +162,8 @@
             COMPLICATION_TYPE_DATE,
             COMPLICATION_TYPE_WEATHER,
             COMPLICATION_TYPE_AIR_QUALITY,
-            COMPLICATION_TYPE_CAST_INFO
+            COMPLICATION_TYPE_CAST_INFO,
+            COMPLICATION_TYPE_HOME_CONTROLS
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface ComplicationType {}
@@ -173,6 +174,7 @@
     int COMPLICATION_TYPE_WEATHER = 1 << 2;
     int COMPLICATION_TYPE_AIR_QUALITY = 1 << 3;
     int COMPLICATION_TYPE_CAST_INFO = 1 << 4;
+    int COMPLICATION_TYPE_HOME_CONTROLS = 1 << 5;
 
     /**
      * The {@link Host} interface specifies a way a {@link Complication} to communicate with its
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
index a4a0075..dcab90f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationUtils.java
@@ -19,6 +19,7 @@
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_AIR_QUALITY;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_NONE;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_WEATHER;
@@ -48,6 +49,8 @@
                 return COMPLICATION_TYPE_AIR_QUALITY;
             case DreamBackend.COMPLICATION_TYPE_CAST_INFO:
                 return COMPLICATION_TYPE_CAST_INFO;
+            case DreamBackend.COMPLICATION_TYPE_HOME_CONTROLS:
+                return COMPLICATION_TYPE_HOME_CONTROLS;
             default:
                 return COMPLICATION_TYPE_NONE;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockDateComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockDateComplication.java
deleted file mode 100644
index 1ca06b2..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockDateComplication.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * 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.complication;
-
-import static com.android.systemui.dreams.complication.dagger.DreamClockDateComplicationModule.DREAM_CLOCK_DATE_COMPLICATION_LAYOUT_PARAMS;
-import static com.android.systemui.dreams.complication.dagger.DreamClockDateComplicationModule.DREAM_CLOCK_DATE_COMPLICATION_VIEW;
-
-import android.content.Context;
-import android.view.View;
-
-import com.android.systemui.CoreStartable;
-import com.android.systemui.dreams.DreamOverlayStateController;
-
-import javax.inject.Inject;
-import javax.inject.Named;
-import javax.inject.Provider;
-
-/**
- * Clock Date Complication that produce Clock Date view holder.
- */
-public class DreamClockDateComplication implements Complication {
-    private final Provider<DreamClockDateViewHolder> mDreamClockDateViewHolderProvider;
-
-    /**
-     * Default constructor for {@link DreamClockDateComplication}.
-     */
-    @Inject
-    public DreamClockDateComplication(
-            Provider<DreamClockDateViewHolder> dreamClockDateViewHolderProvider) {
-        mDreamClockDateViewHolderProvider = dreamClockDateViewHolderProvider;
-    }
-
-    @Override
-    public int getRequiredTypeAvailability() {
-        return COMPLICATION_TYPE_DATE;
-    }
-
-    /**
-     * Create {@link DreamClockDateViewHolder}.
-     */
-    @Override
-    public ViewHolder createView(ComplicationViewModel model) {
-        return mDreamClockDateViewHolderProvider.get();
-    }
-
-    /**
-     * {@link CoreStartable} responsible for registering {@link DreamClockDateComplication} with
-     * SystemUI.
-     */
-    public static class Registrant extends CoreStartable {
-        private final DreamOverlayStateController mDreamOverlayStateController;
-        private final DreamClockDateComplication mComplication;
-
-        /**
-         * Default constructor to register {@link DreamClockDateComplication}.
-         */
-        @Inject
-        public Registrant(Context context,
-                DreamOverlayStateController dreamOverlayStateController,
-                DreamClockDateComplication dreamClockDateComplication) {
-            super(context);
-            mDreamOverlayStateController = dreamOverlayStateController;
-            mComplication = dreamClockDateComplication;
-        }
-
-        @Override
-        public void start() {
-            mDreamOverlayStateController.addComplication(mComplication);
-        }
-    }
-
-    /**
-     * {@link ViewHolder} to contain value/logic associated with {@link DreamClockDateComplication}.
-     */
-    public static class DreamClockDateViewHolder implements ViewHolder {
-        private final View mView;
-        private final ComplicationLayoutParams mLayoutParams;
-
-        @Inject
-        DreamClockDateViewHolder(@Named(DREAM_CLOCK_DATE_COMPLICATION_VIEW) View view,
-                @Named(DREAM_CLOCK_DATE_COMPLICATION_LAYOUT_PARAMS)
-                        ComplicationLayoutParams layoutParams) {
-            mView = view;
-            mLayoutParams = layoutParams;
-        }
-
-        @Override
-        public View getView() {
-            return mView;
-        }
-
-        @Override
-        public ComplicationLayoutParams getLayoutParams() {
-            return mLayoutParams;
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
index 7f67ecd..675a2f4 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamClockTimeComplication.java
@@ -16,8 +16,8 @@
 
 package com.android.systemui.dreams.complication;
 
-import static com.android.systemui.dreams.complication.dagger.DreamClockTimeComplicationModule.DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS;
 import static com.android.systemui.dreams.complication.dagger.DreamClockTimeComplicationModule.DREAM_CLOCK_TIME_COMPLICATION_VIEW;
+import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS;
 
 import android.content.Context;
 import android.view.View;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
index 1a9d9b5..1c72e49 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamHomeControlsComplication.java
@@ -19,8 +19,8 @@
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE;
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE_AFTER_UNLOCK;
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.UNAVAILABLE;
-import static com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent.DreamHomeControlsModule.DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS;
 import static com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent.DreamHomeControlsModule.DREAM_HOME_CONTROLS_CHIP_VIEW;
+import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS;
 
 import android.content.Context;
 import android.content.Intent;
@@ -62,7 +62,7 @@
 
     @Override
     public int getRequiredTypeAvailability() {
-        return COMPLICATION_TYPE_NONE;
+        return COMPLICATION_TYPE_HOME_CONTROLS;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
similarity index 79%
rename from packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java
rename to packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
index be94e50..ac6edba 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/SmartSpaceComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/SmartSpaceComplication.java
@@ -14,7 +14,9 @@
  * limitations under the License.
  */
 
-package com.android.systemui.dreams;
+package com.android.systemui.dreams.complication;
+
+import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_SMARTSPACE_LAYOUT_PARAMS;
 
 import android.content.Context;
 import android.os.Parcelable;
@@ -23,21 +25,33 @@
 import android.widget.FrameLayout;
 
 import com.android.systemui.CoreStartable;
-import com.android.systemui.dreams.complication.Complication;
-import com.android.systemui.dreams.complication.ComplicationLayoutParams;
-import com.android.systemui.dreams.complication.ComplicationViewModel;
+import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.smartspace.DreamSmartspaceController;
 import com.android.systemui.plugins.BcSmartspaceDataPlugin;
 
 import java.util.List;
 
 import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Provider;
 
 /**
  * {@link SmartSpaceComplication} embodies the SmartSpace view found on the lockscreen as a
  * {@link Complication}
  */
 public class SmartSpaceComplication implements Complication {
+    private final Provider<SmartSpaceComplicationViewHolder> mViewHolderProvider;
+
+    @Inject
+    public SmartSpaceComplication(Provider<SmartSpaceComplicationViewHolder> viewHolderProvider) {
+        mViewHolderProvider = viewHolderProvider;
+    }
+
+    @Override
+    public ViewHolder createView(ComplicationViewModel model) {
+        return mViewHolderProvider.get();
+    }
+
     /**
      * {@link CoreStartable} responsbile for registering {@link SmartSpaceComplication} with
      * SystemUI.
@@ -89,17 +103,20 @@
         }
     }
 
-    private static class SmartSpaceComplicationViewHolder implements ViewHolder {
+    static class SmartSpaceComplicationViewHolder implements ViewHolder {
         private View mView = null;
-        private static final int SMARTSPACE_COMPLICATION_WEIGHT = 10;
         private final DreamSmartspaceController mSmartSpaceController;
         private final Context mContext;
+        private final ComplicationLayoutParams mLayoutParams;
 
+        @Inject
         protected SmartSpaceComplicationViewHolder(
                 Context context,
-                DreamSmartspaceController smartSpaceController) {
+                DreamSmartspaceController smartSpaceController,
+                @Named(DREAM_SMARTSPACE_LAYOUT_PARAMS) ComplicationLayoutParams layoutParams) {
             mSmartSpaceController = smartSpaceController;
             mContext = context;
+            mLayoutParams = layoutParams;
         }
 
         @Override
@@ -119,25 +136,7 @@
 
         @Override
         public ComplicationLayoutParams getLayoutParams() {
-            return new ComplicationLayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT,
-                    ComplicationLayoutParams.POSITION_TOP | ComplicationLayoutParams.POSITION_START,
-                    ComplicationLayoutParams.DIRECTION_DOWN,
-                    SMARTSPACE_COMPLICATION_WEIGHT, true);
+            return mLayoutParams;
         }
     }
-
-    private final DreamSmartspaceController mSmartSpaceController;
-    private final Context mContext;
-
-    @Inject
-    public SmartSpaceComplication(Context context,
-            DreamSmartspaceController smartSpaceController) {
-        mContext = context;
-        mSmartSpaceController = smartSpaceController;
-    }
-
-    @Override
-    public ViewHolder createView(ComplicationViewModel model) {
-        return new SmartSpaceComplicationViewHolder(mContext, mSmartSpaceController);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockDateComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockDateComplicationModule.java
deleted file mode 100644
index 3ab26ce..0000000
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockDateComplicationModule.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.complication.dagger;
-
-
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.internal.util.Preconditions;
-import com.android.systemui.R;
-import com.android.systemui.dreams.complication.ComplicationLayoutParams;
-import com.android.systemui.dreams.complication.DreamClockDateComplication;
-
-import javax.inject.Named;
-
-import dagger.Module;
-import dagger.Provides;
-
-/**
- * Module for providing {@link DreamClockDateComplication}.
- */
-@Module
-public interface DreamClockDateComplicationModule {
-    String DREAM_CLOCK_DATE_COMPLICATION_VIEW = "clock_date_complication_view";
-    String DREAM_CLOCK_DATE_COMPLICATION_LAYOUT_PARAMS =
-            "clock_date_complication_layout_params";
-    // Order weight of insert into parent container
-    //TODO(b/217199227): move to a single location.
-    int INSERT_ORDER_WEIGHT = 3;
-
-    /**
-     * Provides the complication view.
-     */
-    @Provides
-    @Named(DREAM_CLOCK_DATE_COMPLICATION_VIEW)
-    static View provideComplicationView(LayoutInflater layoutInflater) {
-        return Preconditions.checkNotNull(
-                layoutInflater.inflate(R.layout.dream_overlay_complication_clock_date,
-                        null, false),
-                "R.layout.dream_overlay_complication_clock_date did not properly inflated");
-    }
-
-    /**
-     * Provides the layout parameters for the complication view.
-     */
-    @Provides
-    @Named(DREAM_CLOCK_DATE_COMPLICATION_LAYOUT_PARAMS)
-    static ComplicationLayoutParams provideLayoutParams() {
-        return new ComplicationLayoutParams(0,
-                ViewGroup.LayoutParams.WRAP_CONTENT,
-                ComplicationLayoutParams.POSITION_BOTTOM
-                        | ComplicationLayoutParams.POSITION_START,
-                ComplicationLayoutParams.DIRECTION_END,
-                INSERT_ORDER_WEIGHT, /* snapToGuide= */ true);
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java
index 3ad7d3d..5250d44 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamClockTimeComplicationModule.java
@@ -19,12 +19,10 @@
 
 import android.view.LayoutInflater;
 import android.view.View;
-import android.view.ViewGroup;
 import android.widget.TextClock;
 
 import com.android.internal.util.Preconditions;
 import com.android.systemui.R;
-import com.android.systemui.dreams.complication.ComplicationLayoutParams;
 import com.android.systemui.dreams.complication.DreamClockTimeComplication;
 
 import javax.inject.Named;
@@ -38,11 +36,6 @@
 @Module
 public interface DreamClockTimeComplicationModule {
     String DREAM_CLOCK_TIME_COMPLICATION_VIEW = "clock_time_complication_view";
-    String DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS =
-            "clock_time_complication_layout_params";
-    // Order weight of insert into parent container
-    //TODO(b/217199227): move to a single location.
-    int INSERT_ORDER_WEIGHT = 0;
     String TAG_WEIGHT = "'wght' ";
     int WEIGHT = 200;
 
@@ -59,18 +52,4 @@
         view.setFontVariationSettings(TAG_WEIGHT + WEIGHT);
         return view;
     }
-
-    /**
-     * Provides the layout parameters for the complication view.
-     */
-    @Provides
-    @Named(DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS)
-    static ComplicationLayoutParams provideLayoutParams() {
-        return new ComplicationLayoutParams(0,
-                ViewGroup.LayoutParams.WRAP_CONTENT,
-                ComplicationLayoutParams.POSITION_BOTTOM
-                        | ComplicationLayoutParams.POSITION_START,
-                ComplicationLayoutParams.DIRECTION_UP,
-                INSERT_ORDER_WEIGHT);
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java
index 033ce39..cf05d2d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/DreamHomeControlsComplicationComponent.java
@@ -18,13 +18,10 @@
 
 import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
-import android.content.res.Resources;
 import android.view.LayoutInflater;
 import android.widget.ImageView;
 
 import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.dreams.complication.ComplicationLayoutParams;
 import com.android.systemui.dreams.complication.DreamHomeControlsComplication;
 
 import java.lang.annotation.Documented;
@@ -70,12 +67,6 @@
     @Module
     interface DreamHomeControlsModule {
         String DREAM_HOME_CONTROLS_CHIP_VIEW = "dream_home_controls_chip_view";
-        String DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS = "home_controls_chip_layout_params";
-
-        // TODO(b/217199227): move to a single location.
-        // Weight of order in the parent container. The home controls complication should have low
-        // weight and be placed at the end.
-        int INSERT_ORDER_WEIGHT = 0;
 
         /**
          * Provides the dream home controls chip view.
@@ -87,22 +78,5 @@
             return (ImageView) layoutInflater.inflate(R.layout.dream_overlay_home_controls_chip,
                     null, false);
         }
-
-        /**
-         * Provides the layout parameters for the dream home controls complication.
-         */
-        @Provides
-        @DreamHomeControlsComplicationScope
-        @Named(DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS)
-        static ComplicationLayoutParams provideLayoutParams(@Main Resources res) {
-            return new ComplicationLayoutParams(
-                    res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width),
-                    res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
-                    ComplicationLayoutParams.POSITION_BOTTOM
-                            | ComplicationLayoutParams.POSITION_START,
-                    ComplicationLayoutParams.DIRECTION_END,
-                    INSERT_ORDER_WEIGHT);
-        }
     }
-
 }
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
index 4a515f0..eb07238 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/dagger/RegisteredComplicationsModule.java
@@ -16,20 +16,79 @@
 
 package com.android.systemui.dreams.complication.dagger;
 
+import android.content.res.Resources;
+import android.view.ViewGroup;
+
+import com.android.systemui.R;
 import com.android.systemui.dagger.SystemUIBinder;
+import com.android.systemui.dagger.qualifiers.Main;
+import com.android.systemui.dreams.complication.ComplicationLayoutParams;
+
+import javax.inject.Named;
 
 import dagger.Module;
+import dagger.Provides;
 
 /**
  * Module for all components with corresponding dream layer complications registered in
  * {@link SystemUIBinder}.
  */
 @Module(includes = {
-                DreamClockDateComplicationModule.class,
                 DreamClockTimeComplicationModule.class,
         },
         subcomponents = {
                 DreamHomeControlsComplicationComponent.class,
         })
 public interface RegisteredComplicationsModule {
+    String DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS = "time_complication_layout_params";
+    String DREAM_SMARTSPACE_LAYOUT_PARAMS = "smartspace_layout_params";
+    String DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS = "home_controls_chip_layout_params";
+
+    int DREAM_CLOCK_TIME_COMPLICATION_WEIGHT = 1;
+    int DREAM_SMARTSPACE_COMPLICATION_WEIGHT = 0;
+    int DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT = 1;
+
+    /**
+     * Provides layout parameters for the clock time complication.
+     */
+    @Provides
+    @Named(DREAM_CLOCK_TIME_COMPLICATION_LAYOUT_PARAMS)
+    static ComplicationLayoutParams provideClockTimeLayoutParams() {
+        return new ComplicationLayoutParams(0,
+            ViewGroup.LayoutParams.WRAP_CONTENT,
+            ComplicationLayoutParams.POSITION_TOP
+                    | ComplicationLayoutParams.POSITION_START,
+            ComplicationLayoutParams.DIRECTION_DOWN,
+            DREAM_CLOCK_TIME_COMPLICATION_WEIGHT);
+    }
+
+    /**
+     * Provides layout parameters for the home controls complication.
+     */
+    @Provides
+    @Named(DREAM_HOME_CONTROLS_CHIP_LAYOUT_PARAMS)
+    static ComplicationLayoutParams provideHomeControlsChipLayoutParams(@Main Resources res) {
+        return new ComplicationLayoutParams(
+            res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width),
+            res.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
+            ComplicationLayoutParams.POSITION_BOTTOM
+                    | ComplicationLayoutParams.POSITION_START,
+            ComplicationLayoutParams.DIRECTION_END,
+            DREAM_HOME_CONTROLS_CHIP_COMPLICATION_WEIGHT);
+    }
+
+    /**
+     * Provides layout parameters for the smartspace complication.
+     */
+    @Provides
+    @Named(DREAM_SMARTSPACE_LAYOUT_PARAMS)
+    static ComplicationLayoutParams provideSmartspaceLayoutParams() {
+        return new ComplicationLayoutParams(0,
+            ViewGroup.LayoutParams.WRAP_CONTENT,
+            ComplicationLayoutParams.POSITION_TOP
+                    | ComplicationLayoutParams.POSITION_START,
+            ComplicationLayoutParams.DIRECTION_DOWN,
+            DREAM_SMARTSPACE_COMPLICATION_WEIGHT,
+            true /*snapToGuide*/);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 958a219..733b1b6 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -87,11 +87,17 @@
 
     public static final ResourceBooleanFlag FACE_SCANNING_ANIM =
             new ResourceBooleanFlag(205, R.bool.config_enableFaceScanningAnimation);
+
     /**
      * Whether the KeyguardBottomArea(View|Controller) should use the modern architecture or the old
      * one.
      */
-    public static final BooleanFlag MODERN_BOTTOM_AREA = new BooleanFlag(206, false);
+    public static final BooleanFlag MODERN_BOTTOM_AREA = new BooleanFlag(
+            206,
+            /* default= */ false,
+            /* teamfood= */ true);
+
+    public static final BooleanFlag LOCKSCREEN_CUSTOM_CLOCKS = new BooleanFlag(207, false);
 
     /***************************************/
     // 300 - power menu
@@ -196,6 +202,10 @@
             new DeviceConfigBooleanFlag(1102, "record_task_content",
                     NAMESPACE_WINDOW_MANAGER, false, true);
 
+    @Keep
+    public static final SysPropBooleanFlag HIDE_NAVBAR_WINDOW =
+            new SysPropBooleanFlag(1103, "persist.wm.debug.hide_navbar_window", false);
+
     // 1200 - predictive back
     @Keep
     public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
index ab30db2..ca65d12 100644
--- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
+++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java
@@ -947,7 +947,7 @@
             mHandler.postDelayed(new Runnable() {
                 @Override
                 public void run() {
-                    mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN, true, true,
+                    mScreenshotHelper.takeScreenshot(TAKE_SCREENSHOT_FULLSCREEN,
                             SCREENSHOT_GLOBAL_ACTIONS, mHandler, null);
                     mMetricsLogger.action(MetricsEvent.ACTION_SCREENSHOT_POWER_MENU);
                     mUiEventLogger.log(GlobalActionsEvent.GA_SCREENSHOT_PRESS);
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 165af13..4ff008f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -42,6 +42,8 @@
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
+import com.android.systemui.keyguard.domain.usecase.KeyguardUseCaseModule;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
@@ -66,7 +68,11 @@
         KeyguardStatusBarViewComponent.class,
         KeyguardStatusViewComponent.class,
         KeyguardUserSwitcherComponent.class},
-        includes = {FalsingModule.class})
+        includes = {
+            FalsingModule.class,
+            KeyguardRepositoryModule.class,
+            KeyguardUseCaseModule.class,
+        })
 public class KeyguardModule {
     /**
      * Provides our instance of KeyguardViewMediator which is considered optional.
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..3202ecb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,121 @@
+/*
+ *  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.keyguard.data.quickaffordance
+
+import android.content.Context
+import android.content.Intent
+import androidx.annotation.DrawableRes
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.controls.ControlsServiceInfo
+import com.android.systemui.controls.controller.StructureInfo
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.ui.ControlsActivity
+import com.android.systemui.controls.ui.ControlsUiController
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.util.kotlin.getOrNull
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flowOf
+
+/** Home controls quick affordance data source. */
+@SysUISingleton
+class HomeControlsKeyguardQuickAffordanceConfig
+@Inject
+constructor(
+    @Application context: Context,
+    private val component: ControlsComponent,
+) : KeyguardQuickAffordanceConfig {
+
+    private val appContext = context.applicationContext
+
+    override val state: Flow<KeyguardQuickAffordanceConfig.State> =
+        stateInternal(component.getControlsListingController().getOrNull())
+
+    override fun onQuickAffordanceClicked(
+        animationController: ActivityLaunchAnimator.Controller?,
+    ): KeyguardQuickAffordanceConfig.OnClickedResult {
+        return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
+            intent =
+                Intent(appContext, ControlsActivity::class.java)
+                    .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
+                    .putExtra(
+                        ControlsUiController.EXTRA_ANIMATE,
+                        true,
+                    ),
+            canShowWhileLocked = component.canShowWhileLockedSetting.value,
+        )
+    }
+
+    private fun stateInternal(
+        listingController: ControlsListingController?,
+    ): Flow<KeyguardQuickAffordanceConfig.State> {
+        if (listingController == null) {
+            return flowOf(KeyguardQuickAffordanceConfig.State.Hidden)
+        }
+
+        return conflatedCallbackFlow {
+            val callback =
+                object : ControlsListingController.ControlsListingCallback {
+                    override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) {
+                        val favorites: List<StructureInfo>? =
+                            component.getControlsController().getOrNull()?.getFavorites()
+
+                        trySendWithFailureLogging(
+                            state(
+                                isFeatureEnabled = component.isEnabled(),
+                                hasFavorites = favorites?.isNotEmpty() == true,
+                                hasServiceInfos = serviceInfos.isNotEmpty(),
+                                iconResourceId = component.getTileImageId(),
+                            ),
+                            TAG,
+                        )
+                    }
+                }
+
+            listingController.addCallback(callback)
+
+            awaitClose { listingController.removeCallback(callback) }
+        }
+    }
+
+    private fun state(
+        isFeatureEnabled: Boolean,
+        hasFavorites: Boolean,
+        hasServiceInfos: Boolean,
+        @DrawableRes iconResourceId: Int?,
+    ): KeyguardQuickAffordanceConfig.State {
+        return if (isFeatureEnabled && hasFavorites && hasServiceInfos && iconResourceId != null) {
+            KeyguardQuickAffordanceConfig.State.Visible(
+                icon = ContainedDrawable.WithResource(iconResourceId),
+                contentDescriptionResourceId = component.getTileTitleId(),
+            )
+        } else {
+            KeyguardQuickAffordanceConfig.State.Hidden
+        }
+    }
+
+    companion object {
+        private const val TAG = "HomeControlsKeyguardQuickAffordanceConfig"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..67a776e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,73 @@
+/*
+ *  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.keyguard.data.quickaffordance
+
+import android.content.Intent
+import androidx.annotation.StringRes
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.containeddrawable.ContainedDrawable
+import kotlinx.coroutines.flow.Flow
+
+/** Defines interface that can act as data source for a single quick affordance model. */
+interface KeyguardQuickAffordanceConfig {
+
+    val state: Flow<State>
+
+    fun onQuickAffordanceClicked(
+        animationController: ActivityLaunchAnimator.Controller?
+    ): OnClickedResult
+
+    /**
+     * Encapsulates the state of a "quick affordance" in the keyguard bottom area (for example, a
+     * button on the lock-screen).
+     */
+    sealed class State {
+
+        /** No affordance should show up. */
+        object Hidden : State()
+
+        /** An affordance is visible. */
+        data class Visible(
+            /** An icon for the affordance. */
+            val icon: ContainedDrawable,
+            /**
+             * Resource ID for a string to use for the accessibility content description text of the
+             * affordance.
+             */
+            @StringRes val contentDescriptionResourceId: Int,
+        ) : State()
+    }
+
+    sealed class OnClickedResult {
+        /**
+         * Returning this as a result from the [onQuickAffordanceClicked] method means that the
+         * implementation has taken care of the click, the system will do nothing.
+         */
+        object Handled : OnClickedResult()
+
+        /**
+         * Returning this as a result from the [onQuickAffordanceClicked] method means that the
+         * implementation has _not_ taken care of the click and the system should start an activity
+         * using the given [Intent].
+         */
+        data class StartActivity(
+            val intent: Intent,
+            val canShowWhileLocked: Boolean,
+        ) : OnClickedResult()
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..ea6497e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,90 @@
+/*
+ *  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.keyguard.data.quickaffordance
+
+import com.android.systemui.R
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** QR code scanner quick affordance data source. */
+@SysUISingleton
+class QrCodeScannerKeyguardQuickAffordanceConfig
+@Inject
+constructor(
+    private val controller: QRCodeScannerController,
+) : KeyguardQuickAffordanceConfig {
+
+    override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow {
+        val callback =
+            object : QRCodeScannerController.Callback {
+                override fun onQRCodeScannerActivityChanged() {
+                    trySendWithFailureLogging(state(), TAG)
+                }
+                override fun onQRCodeScannerPreferenceChanged() {
+                    trySendWithFailureLogging(state(), TAG)
+                }
+            }
+
+        controller.addCallback(callback)
+        controller.registerQRCodeScannerChangeObservers(
+            QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE,
+            QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE
+        )
+        // Registering does not push an initial update.
+        trySendWithFailureLogging(state(), "initial state", TAG)
+
+        awaitClose {
+            controller.unregisterQRCodeScannerChangeObservers(
+                QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE,
+                QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE
+            )
+            controller.removeCallback(callback)
+        }
+    }
+
+    override fun onQuickAffordanceClicked(
+        animationController: ActivityLaunchAnimator.Controller?,
+    ): KeyguardQuickAffordanceConfig.OnClickedResult {
+        return KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
+            intent = controller.intent,
+            canShowWhileLocked = true,
+        )
+    }
+
+    private fun state(): KeyguardQuickAffordanceConfig.State {
+        return if (controller.isEnabledForLockScreenButton) {
+            KeyguardQuickAffordanceConfig.State.Visible(
+                icon = ContainedDrawable.WithResource(R.drawable.ic_qr_code_scanner),
+                contentDescriptionResourceId = R.string.accessibility_qr_code_scanner_button,
+            )
+        } else {
+            KeyguardQuickAffordanceConfig.State.Hidden
+        }
+    }
+
+    companion object {
+        private const val TAG = "QrCodeScannerKeyguardQuickAffordanceConfig"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..cc5a997
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,114 @@
+/*
+ *  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.keyguard.data.quickaffordance
+
+import android.graphics.drawable.Drawable
+import android.service.quickaccesswallet.GetWalletCardsError
+import android.service.quickaccesswallet.GetWalletCardsResponse
+import android.service.quickaccesswallet.QuickAccessWalletClient
+import android.util.Log
+import com.android.systemui.R
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.wallet.controller.QuickAccessWalletController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Quick access wallet quick affordance data source. */
+@SysUISingleton
+class QuickAccessWalletKeyguardQuickAffordanceConfig
+@Inject
+constructor(
+    private val walletController: QuickAccessWalletController,
+    private val activityStarter: ActivityStarter,
+) : KeyguardQuickAffordanceConfig {
+
+    override val state: Flow<KeyguardQuickAffordanceConfig.State> = conflatedCallbackFlow {
+        val callback =
+            object : QuickAccessWalletClient.OnWalletCardsRetrievedCallback {
+                override fun onWalletCardsRetrieved(response: GetWalletCardsResponse?) {
+                    trySendWithFailureLogging(
+                        state(
+                            isFeatureEnabled = walletController.isWalletEnabled,
+                            hasCard = response?.walletCards?.isNotEmpty() == true,
+                            tileIcon = walletController.walletClient.tileIcon,
+                        ),
+                        TAG,
+                    )
+                }
+
+                override fun onWalletCardRetrievalError(error: GetWalletCardsError?) {
+                    Log.e(TAG, "Wallet card retrieval error, message: \"${error?.message}\"")
+                    trySendWithFailureLogging(
+                        KeyguardQuickAffordanceConfig.State.Hidden,
+                        TAG,
+                    )
+                }
+            }
+
+        walletController.setupWalletChangeObservers(
+            callback,
+            QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
+            QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+        )
+        walletController.updateWalletPreference()
+        walletController.queryWalletCards(callback)
+
+        awaitClose {
+            walletController.unregisterWalletChangeObservers(
+                QuickAccessWalletController.WalletChangeEvent.WALLET_PREFERENCE_CHANGE,
+                QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE
+            )
+        }
+    }
+
+    override fun onQuickAffordanceClicked(
+        animationController: ActivityLaunchAnimator.Controller?,
+    ): KeyguardQuickAffordanceConfig.OnClickedResult {
+        walletController.startQuickAccessUiIntent(
+            activityStarter,
+            animationController,
+            /* hasCard= */ true,
+        )
+        return KeyguardQuickAffordanceConfig.OnClickedResult.Handled
+    }
+
+    private fun state(
+        isFeatureEnabled: Boolean,
+        hasCard: Boolean,
+        tileIcon: Drawable?,
+    ): KeyguardQuickAffordanceConfig.State {
+        return if (isFeatureEnabled && hasCard && tileIcon != null) {
+            KeyguardQuickAffordanceConfig.State.Visible(
+                icon = ContainedDrawable.WithDrawable(tileIcon),
+                contentDescriptionResourceId = R.string.accessibility_wallet_button,
+            )
+        } else {
+            KeyguardQuickAffordanceConfig.State.Hidden
+        }
+    }
+
+    companion object {
+        private const val TAG = "QuickAccessWalletKeyguardQuickAffordanceConfig"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceConfigs.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceConfigs.kt
new file mode 100644
index 0000000..7164215
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceConfigs.kt
@@ -0,0 +1,67 @@
+/*
+ *  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.keyguard.data.config
+
+import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.QrCodeScannerKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.QuickAccessWalletKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
+import javax.inject.Inject
+import kotlin.reflect.KClass
+
+/** Injectable provider of the positioning of the known quick affordance configs. */
+interface KeyguardQuickAffordanceConfigs {
+    fun getAll(position: KeyguardQuickAffordancePosition): List<KeyguardQuickAffordanceConfig>
+    fun get(configClass: KClass<out KeyguardQuickAffordanceConfig>): KeyguardQuickAffordanceConfig
+}
+
+class KeyguardQuickAffordanceConfigsImpl
+@Inject
+constructor(
+    homeControls: HomeControlsKeyguardQuickAffordanceConfig,
+    quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
+    qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
+) : KeyguardQuickAffordanceConfigs {
+    private val configsByPosition =
+        mapOf(
+            KeyguardQuickAffordancePosition.BOTTOM_START to
+                listOf(
+                    homeControls,
+                ),
+            KeyguardQuickAffordancePosition.BOTTOM_END to
+                listOf(
+                    quickAccessWallet,
+                    qrCodeScanner,
+                ),
+        )
+    private val configByClass =
+        configsByPosition.values.flatten().associateBy { config -> config::class }
+
+    override fun getAll(
+        position: KeyguardQuickAffordancePosition,
+    ): List<KeyguardQuickAffordanceConfig> {
+        return configsByPosition.getValue(position)
+    }
+
+    override fun get(
+        configClass: KClass<out KeyguardQuickAffordanceConfig>
+    ): KeyguardQuickAffordanceConfig {
+        return configByClass.getValue(configClass)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
new file mode 100644
index 0000000..43c4fa0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.keyguard.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.data.config.KeyguardQuickAffordanceConfigs
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.State
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+
+/** Defines interface for classes that encapsulate quick affordance state for the keyguard. */
+interface KeyguardQuickAffordanceRepository {
+    fun affordance(position: KeyguardQuickAffordancePosition): Flow<KeyguardQuickAffordanceModel>
+}
+
+/** Real implementation of [KeyguardQuickAffordanceRepository] */
+@SysUISingleton
+class KeyguardQuickAffordanceRepositoryImpl
+@Inject
+constructor(
+    private val configs: KeyguardQuickAffordanceConfigs,
+) : KeyguardQuickAffordanceRepository {
+
+    /** Returns an observable for the quick affordance model in the given position. */
+    override fun affordance(
+        position: KeyguardQuickAffordancePosition
+    ): Flow<KeyguardQuickAffordanceModel> {
+        val configs = configs.getAll(position)
+        return combine(configs.map { config -> config.state }) { states ->
+            val index = states.indexOfFirst { state -> state is State.Visible }
+            val visibleState =
+                if (index != -1) {
+                    states[index] as State.Visible
+                } else {
+                    null
+                }
+            if (visibleState != null) {
+                KeyguardQuickAffordanceModel.Visible(
+                    configKey = configs[index]::class,
+                    icon = visibleState.icon,
+                    contentDescriptionResourceId = visibleState.contentDescriptionResourceId,
+                )
+            } else {
+                KeyguardQuickAffordanceModel.Hidden
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
new file mode 100644
index 0000000..62cf1a6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -0,0 +1,181 @@
+/*
+ * 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.keyguard.data.repository
+
+import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.common.data.model.Position
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Defines interface for classes that encapsulate application state for the keyguard. */
+interface KeyguardRepository {
+    /**
+     * Observable for whether the bottom area UI should animate the transition out of doze state.
+     *
+     * To learn more about doze state, please see [isDozing].
+     */
+    val animateBottomAreaDozingTransitions: StateFlow<Boolean>
+
+    /**
+     * Observable for the current amount of alpha that should be used for rendering the bottom area.
+     * UI.
+     */
+    val bottomAreaAlpha: StateFlow<Float>
+
+    /**
+     * Observable of the relative offset of the lock-screen clock from its natural position on the
+     * screen.
+     */
+    val clockPosition: StateFlow<Position>
+
+    /**
+     * Observable for whether the keyguard is showing.
+     *
+     * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in
+     * the z-order (which is not really above the system UI window, but rather - the lock-screen
+     * becomes invisible to reveal the "occluding activity").
+     */
+    val isKeyguardShowing: Flow<Boolean>
+
+    /**
+     * Observable for whether we are in doze state.
+     *
+     * Doze state is the same as "Always on Display" or "AOD". It is the state that the device can
+     * enter to conserve battery when the device is locked and inactive.
+     *
+     * Note that it is possible for the system to be transitioning into doze while this flow still
+     * returns `false`. In order to account for that, observers should also use the [dozeAmount]
+     * flow to check if it's greater than `0`
+     */
+    val isDozing: Flow<Boolean>
+
+    /**
+     * Observable for the amount of doze we are currently in.
+     *
+     * While in doze state, this amount can change - driving a cycle of animations designed to avoid
+     * pixel burn-in, etc.
+     *
+     * Also note that the value here may be greater than `0` while [isDozing] is still `false`, this
+     * happens during an animation/transition into doze mode. An observer would be wise to account
+     * for both flows if needed.
+     */
+    val dozeAmount: Flow<Float>
+
+    /** Sets whether the bottom area UI should animate the transition out of doze state. */
+    fun setAnimateDozingTransitions(animate: Boolean)
+
+    /** Sets the current amount of alpha that should be used for rendering the bottom area. */
+    fun setBottomAreaAlpha(alpha: Float)
+
+    /**
+     * Sets the relative offset of the lock-screen clock from its natural position on the screen.
+     */
+    fun setClockPosition(x: Int, y: Int)
+}
+
+/** Encapsulates application state for the keyguard. */
+@SysUISingleton
+class KeyguardRepositoryImpl
+@Inject
+constructor(
+    statusBarStateController: StatusBarStateController,
+    keyguardStateController: KeyguardStateController,
+) : KeyguardRepository {
+    private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
+    override val animateBottomAreaDozingTransitions =
+        _animateBottomAreaDozingTransitions.asStateFlow()
+
+    private val _bottomAreaAlpha = MutableStateFlow(1f)
+    override val bottomAreaAlpha = _bottomAreaAlpha.asStateFlow()
+
+    private val _clockPosition = MutableStateFlow(Position(0, 0))
+    override val clockPosition = _clockPosition.asStateFlow()
+
+    override val isKeyguardShowing: Flow<Boolean> = conflatedCallbackFlow {
+        val callback =
+            object : KeyguardStateController.Callback {
+                override fun onKeyguardShowingChanged() {
+                    trySendWithFailureLogging(
+                        keyguardStateController.isShowing,
+                        TAG,
+                        "updated isKeyguardShowing"
+                    )
+                }
+            }
+
+        keyguardStateController.addCallback(callback)
+        // Adding the callback does not send an initial update.
+        trySendWithFailureLogging(
+            keyguardStateController.isShowing,
+            TAG,
+            "initial isKeyguardShowing"
+        )
+
+        awaitClose { keyguardStateController.removeCallback(callback) }
+    }
+
+    override val isDozing: Flow<Boolean> = conflatedCallbackFlow {
+        val callback =
+            object : StatusBarStateController.StateListener {
+                override fun onDozingChanged(isDozing: Boolean) {
+                    trySendWithFailureLogging(isDozing, TAG, "updated isDozing")
+                }
+            }
+
+        statusBarStateController.addCallback(callback)
+        trySendWithFailureLogging(statusBarStateController.isDozing, TAG, "initial isDozing")
+
+        awaitClose { statusBarStateController.removeCallback(callback) }
+    }
+    override val dozeAmount: Flow<Float> = conflatedCallbackFlow {
+        val callback =
+            object : StatusBarStateController.StateListener {
+                override fun onDozeAmountChanged(linear: Float, eased: Float) {
+                    trySendWithFailureLogging(eased, TAG, "updated dozeAmount")
+                }
+            }
+
+        statusBarStateController.addCallback(callback)
+        trySendWithFailureLogging(statusBarStateController.dozeAmount, TAG, "initial dozeAmount")
+
+        awaitClose { statusBarStateController.removeCallback(callback) }
+    }
+
+    override fun setAnimateDozingTransitions(animate: Boolean) {
+        _animateBottomAreaDozingTransitions.value = animate
+    }
+
+    override fun setBottomAreaAlpha(alpha: Float) {
+        _bottomAreaAlpha.value = alpha
+    }
+
+    override fun setClockPosition(x: Int, y: Int) {
+        _clockPosition.value = Position(x, y)
+    }
+
+    companion object {
+        private const val TAG = "KeyguardRepositoryImpl"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
new file mode 100644
index 0000000..1a5670c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryModule.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.keyguard.data.repository
+
+import com.android.systemui.keyguard.data.config.KeyguardQuickAffordanceConfigs
+import com.android.systemui.keyguard.data.config.KeyguardQuickAffordanceConfigsImpl
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface KeyguardRepositoryModule {
+    @Binds fun keyguardRepository(impl: KeyguardRepositoryImpl): KeyguardRepository
+
+    @Binds
+    fun keyguardQuickAffordanceRepository(
+        impl: KeyguardQuickAffordanceRepositoryImpl
+    ): KeyguardQuickAffordanceRepository
+
+    @Binds
+    fun keyguardQuickAffordanceConfigs(
+        impl: KeyguardQuickAffordanceConfigsImpl
+    ): KeyguardQuickAffordanceConfigs
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/KeyguardUseCaseModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/KeyguardUseCaseModule.kt
new file mode 100644
index 0000000..c44c2c9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/KeyguardUseCaseModule.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import dagger.Binds
+import dagger.Module
+
+@Module
+interface KeyguardUseCaseModule {
+
+    @Binds
+    fun launchQuickAffordance(
+        impl: LaunchKeyguardQuickAffordanceUseCaseImpl
+    ): LaunchKeyguardQuickAffordanceUseCase
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/LaunchKeyguardQuickAffordanceUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/LaunchKeyguardQuickAffordanceUseCase.kt
new file mode 100644
index 0000000..3d60399
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/LaunchKeyguardQuickAffordanceUseCase.kt
@@ -0,0 +1,76 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import android.content.Intent
+import com.android.internal.widget.LockPatternUtils
+import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.StrongAuthFlags
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import javax.inject.Inject
+
+/** Defines interface for classes that can launch a quick affordance. */
+interface LaunchKeyguardQuickAffordanceUseCase {
+    operator fun invoke(
+        intent: Intent,
+        canShowWhileLocked: Boolean,
+        animationController: ActivityLaunchAnimator.Controller?,
+    )
+}
+
+/** Real implementation of [LaunchKeyguardQuickAffordanceUseCase] */
+class LaunchKeyguardQuickAffordanceUseCaseImpl
+@Inject
+constructor(
+    private val lockPatternUtils: LockPatternUtils,
+    private val keyguardStateController: KeyguardStateController,
+    private val userTracker: UserTracker,
+    private val activityStarter: ActivityStarter,
+) : LaunchKeyguardQuickAffordanceUseCase {
+    override operator fun invoke(
+        intent: Intent,
+        canShowWhileLocked: Boolean,
+        animationController: ActivityLaunchAnimator.Controller?,
+    ) {
+        @StrongAuthFlags
+        val strongAuthFlags =
+            lockPatternUtils.getStrongAuthForUser(userTracker.userHandle.identifier)
+        val needsToUnlockFirst =
+            when {
+                strongAuthFlags ==
+                    LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT -> true
+                !canShowWhileLocked && !keyguardStateController.isUnlocked -> true
+                else -> false
+            }
+        if (needsToUnlockFirst) {
+            activityStarter.postStartActivityDismissingKeyguard(
+                intent,
+                0 /* delay */,
+                animationController
+            )
+        } else {
+            activityStarter.startActivity(
+                intent,
+                true /* dismissShade */,
+                animationController,
+                true /* showOverLockscreenWhenLocked */,
+            )
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveAnimateBottomAreaTransitionsUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveAnimateBottomAreaTransitionsUseCase.kt
new file mode 100644
index 0000000..ca37727
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveAnimateBottomAreaTransitionsUseCase.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Use-case for observing whether doze state transitions should animate the bottom area */
+class ObserveAnimateBottomAreaTransitionsUseCase
+@Inject
+constructor(
+    private val repository: KeyguardRepository,
+) {
+    operator fun invoke(): Flow<Boolean> {
+        return repository.animateBottomAreaDozingTransitions
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveBottomAreaAlphaUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveBottomAreaAlphaUseCase.kt
new file mode 100644
index 0000000..151b704
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveBottomAreaAlphaUseCase.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Use-case for observing the alpha of the bottom area */
+class ObserveBottomAreaAlphaUseCase
+@Inject
+constructor(
+    private val repository: KeyguardRepository,
+) {
+    operator fun invoke(): Flow<Float> {
+        return repository.bottomAreaAlpha
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveClockPositionUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveClockPositionUseCase.kt
new file mode 100644
index 0000000..02c5737
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveClockPositionUseCase.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.common.domain.model.Position
+import com.android.systemui.common.domain.model.Position.Companion.toDomainLayer
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Use-case for observing the position of the clock. */
+class ObserveClockPositionUseCase
+@Inject
+constructor(
+    private val repository: KeyguardRepository,
+) {
+    operator fun invoke(): Flow<Position> {
+        return repository.clockPosition.map { it.toDomainLayer() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveDozeAmountUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveDozeAmountUseCase.kt
new file mode 100644
index 0000000..56d6182
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveDozeAmountUseCase.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Use-case for observing the amount of doze the system is in. */
+class ObserveDozeAmountUseCase
+@Inject
+constructor(
+    private val repository: KeyguardRepository,
+) {
+    operator fun invoke(): Flow<Float> {
+        return repository.dozeAmount
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveIsDozingUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveIsDozingUseCase.kt
new file mode 100644
index 0000000..1d241d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveIsDozingUseCase.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Use-case for observing whether we are dozing. */
+class ObserveIsDozingUseCase
+@Inject
+constructor(
+    private val repository: KeyguardRepository,
+) {
+    operator fun invoke(): Flow<Boolean> {
+        return repository.isDozing
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveIsKeyguardShowingUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveIsKeyguardShowingUseCase.kt
new file mode 100644
index 0000000..11af123
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveIsKeyguardShowingUseCase.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.systemui.keyguard.domain.usecase
+
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Use-case for observing whether the keyguard is currently being shown.
+ *
+ * Note: this is also `true` when the lock-screen is occluded with an `Activity` "above" it in the
+ * z-order (which is not really above the system UI window, but rather - the lock-screen becomes
+ * invisible to reveal the "occluding activity").
+ */
+class ObserveIsKeyguardShowingUseCase
+@Inject
+constructor(
+    private val repository: KeyguardRepository,
+) {
+    operator fun invoke(): Flow<Boolean> {
+        return repository.isKeyguardShowing
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveKeyguardQuickAffordanceUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveKeyguardQuickAffordanceUseCase.kt
new file mode 100644
index 0000000..eef8ec3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/ObserveKeyguardQuickAffordanceUseCase.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+
+/** Use-case for observing the model of a quick affordance in the keyguard. */
+class ObserveKeyguardQuickAffordanceUseCase
+@Inject
+constructor(
+    private val repository: KeyguardQuickAffordanceRepository,
+    private val isDozingUseCase: ObserveIsDozingUseCase,
+    private val isKeyguardShowingUseCase: ObserveIsKeyguardShowingUseCase,
+) {
+    operator fun invoke(
+        position: KeyguardQuickAffordancePosition
+    ): Flow<KeyguardQuickAffordanceModel> {
+        return combine(
+            repository.affordance(position),
+            isDozingUseCase(),
+            isKeyguardShowingUseCase(),
+        ) { affordance, isDozing, isKeyguardShowing ->
+            if (!isDozing && isKeyguardShowing) {
+                affordance
+            } else {
+                KeyguardQuickAffordanceModel.Hidden
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/OnKeyguardQuickAffordanceClickedUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/OnKeyguardQuickAffordanceClickedUseCase.kt
new file mode 100644
index 0000000..f8db90f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/OnKeyguardQuickAffordanceClickedUseCase.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.keyguard.data.config.KeyguardQuickAffordanceConfigs
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import javax.inject.Inject
+import kotlin.reflect.KClass
+
+/** Use-case for handling a click on a keyguard quick affordance (e.g. bottom button). */
+class OnKeyguardQuickAffordanceClickedUseCase
+@Inject
+constructor(
+    private val configs: KeyguardQuickAffordanceConfigs,
+    private val launchAffordanceUseCase: LaunchKeyguardQuickAffordanceUseCase,
+) {
+    operator fun invoke(
+        configKey: KClass<*>,
+        animationController: ActivityLaunchAnimator.Controller?,
+    ) {
+        @Suppress("UNCHECKED_CAST")
+        val config = configs.get(configKey as KClass<out KeyguardQuickAffordanceConfig>)
+        when (val result = config.onQuickAffordanceClicked(animationController)) {
+            is OnClickedResult.StartActivity ->
+                launchAffordanceUseCase(
+                    intent = result.intent,
+                    canShowWhileLocked = result.canShowWhileLocked,
+                    animationController = animationController
+                )
+            is OnClickedResult.Handled -> Unit
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/SetClockPositionUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/SetClockPositionUseCase.kt
new file mode 100644
index 0000000..8f746e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/SetClockPositionUseCase.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import javax.inject.Inject
+
+/** Use-case for setting the updated clock position. */
+class SetClockPositionUseCase
+@Inject
+constructor(
+    private val keyguardRepository: KeyguardRepository,
+) {
+    operator fun invoke(x: Int, y: Int) {
+        keyguardRepository.setClockPosition(x, y)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/SetKeyguardBottomAreaAlphaUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/SetKeyguardBottomAreaAlphaUseCase.kt
new file mode 100644
index 0000000..90be1ec
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/SetKeyguardBottomAreaAlphaUseCase.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import javax.inject.Inject
+
+/** Use-case for setting the alpha that the keyguard bottom area should use */
+class SetKeyguardBottomAreaAlphaUseCase
+@Inject
+constructor(
+    private val repository: KeyguardRepository,
+) {
+    operator fun invoke(alpha: Float) {
+        repository.setBottomAreaAlpha(alpha)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/SetKeyguardBottomAreaAnimateDozingTransitionsUseCase.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/SetKeyguardBottomAreaAnimateDozingTransitionsUseCase.kt
new file mode 100644
index 0000000..007780a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/usecase/SetKeyguardBottomAreaAnimateDozingTransitionsUseCase.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import javax.inject.Inject
+
+/**
+ * Use-case for setting whether the keyguard bottom area should animate the next doze transitions
+ */
+class SetKeyguardBottomAreaAnimateDozingTransitionsUseCase
+@Inject
+constructor(
+    private val repository: KeyguardRepository,
+) {
+    operator fun invoke(animate: Boolean) {
+        repository.setAnimateDozingTransitions(animate)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordanceModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordanceModel.kt
new file mode 100644
index 0000000..09785df
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordanceModel.kt
@@ -0,0 +1,46 @@
+/*
+ *  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.keyguard.shared.model
+
+import androidx.annotation.StringRes
+import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import kotlin.reflect.KClass
+
+/**
+ * Models a "quick affordance" in the keyguard bottom area (for example, a button on the
+ * lock-screen).
+ */
+sealed class KeyguardQuickAffordanceModel {
+
+    /** No affordance should show up. */
+    object Hidden : KeyguardQuickAffordanceModel()
+
+    /** A affordance is visible. */
+    data class Visible(
+        /** Identifier for the affordance this is modeling. */
+        val configKey: KClass<out KeyguardQuickAffordanceConfig>,
+        /** An icon for the affordance. */
+        val icon: ContainedDrawable,
+        /**
+         * Resource ID for a string to use for the accessibility content description text of the
+         * affordance.
+         */
+        @StringRes val contentDescriptionResourceId: Int,
+    ) : KeyguardQuickAffordanceModel()
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePosition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePosition.kt
new file mode 100644
index 0000000..b71e15d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePosition.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.keyguard.shared.model
+
+/** Enumerates all possible positions for quick affordances that can appear on the lock-screen. */
+enum class KeyguardQuickAffordancePosition {
+    BOTTOM_START,
+    BOTTOM_END,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
new file mode 100644
index 0000000..04d30bf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaViewBinder.kt
@@ -0,0 +1,307 @@
+/*
+ * 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.keyguard.ui.binder
+
+import android.util.Size
+import android.util.TypedValue
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewPropertyAnimator
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.core.view.isVisible
+import androidx.core.view.updateLayoutParams
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.settingslib.Utils
+import com.android.systemui.R
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.FalsingManager
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/**
+ * Binds a keyguard bottom area view to its view-model.
+ *
+ * To use this properly, users should maintain a one-to-one relationship between the [View] and the
+ * view-binding, binding each view only once. It is okay and expected for the same instance of the
+ * view-model to be reused for multiple view/view-binder bindings.
+ */
+object KeyguardBottomAreaViewBinder {
+
+    private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
+
+    /**
+     * Defines interface for an object that acts as the binding between the view and its view-model.
+     *
+     * Users of the [KeyguardBottomAreaViewBinder] class should use this to control the binder after
+     * it is bound.
+     */
+    interface Binding {
+        /**
+         * Returns a collection of [ViewPropertyAnimator] instances that can be used to animate the
+         * indication areas.
+         */
+        fun getIndicationAreaAnimators(): List<ViewPropertyAnimator>
+
+        /** Notifies that device configuration has changed. */
+        fun onConfigurationChanged()
+    }
+
+    /** Binds the view to the view-model, continuing to update the former based on the latter. */
+    @JvmStatic
+    fun bind(
+        view: ViewGroup,
+        viewModel: KeyguardBottomAreaViewModel,
+        falsingManager: FalsingManager,
+    ): Binding {
+        val indicationArea: View = view.requireViewById(R.id.keyguard_indication_area)
+        val ambientIndicationArea: View? = view.findViewById(R.id.ambient_indication_container)
+        val startButton: ImageView = view.requireViewById(R.id.start_button)
+        val endButton: ImageView = view.requireViewById(R.id.end_button)
+        val overlayContainer: View = view.requireViewById(R.id.overlay_container)
+        val indicationText: TextView = view.requireViewById(R.id.keyguard_indication_text)
+        val indicationTextBottom: TextView =
+            view.requireViewById(R.id.keyguard_indication_text_bottom)
+
+        view.clipChildren = false
+        view.clipToPadding = false
+
+        val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
+
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    combine(viewModel.startButton, viewModel.animateButtonReveal) {
+                            buttonModel,
+                            animateReveal ->
+                            Pair(buttonModel, animateReveal)
+                        }
+                        .collect { (buttonModel, animateReveal) ->
+                            updateButton(
+                                view = startButton,
+                                viewModel = buttonModel,
+                                animateReveal = animateReveal,
+                                falsingManager = falsingManager,
+                            )
+                        }
+                }
+
+                launch {
+                    combine(viewModel.endButton, viewModel.animateButtonReveal) {
+                            buttonModel,
+                            animateReveal ->
+                            Pair(buttonModel, animateReveal)
+                        }
+                        .collect { (buttonModel, animateReveal) ->
+                            updateButton(
+                                view = endButton,
+                                viewModel = buttonModel,
+                                animateReveal = animateReveal,
+                                falsingManager = falsingManager,
+                            )
+                        }
+                }
+
+                launch {
+                    viewModel.isOverlayContainerVisible.collect { isVisible ->
+                        overlayContainer.visibility =
+                            if (isVisible) {
+                                View.VISIBLE
+                            } else {
+                                View.INVISIBLE
+                            }
+                    }
+                }
+
+                launch {
+                    viewModel.alpha.collect { alpha ->
+                        view.importantForAccessibility =
+                            if (alpha == 0f) {
+                                View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+                            } else {
+                                View.IMPORTANT_FOR_ACCESSIBILITY_AUTO
+                            }
+
+                        ambientIndicationArea?.alpha = alpha
+                        indicationArea.alpha = alpha
+                        startButton.alpha = alpha
+                        endButton.alpha = alpha
+                    }
+                }
+
+                launch {
+                    viewModel.indicationAreaTranslationX.collect { translationX ->
+                        indicationArea.translationX = translationX
+                        ambientIndicationArea?.translationX = translationX
+                    }
+                }
+
+                launch {
+                    combine(
+                            viewModel.isIndicationAreaPadded,
+                            configurationBasedDimensions.map { it.indicationAreaPaddingPx },
+                        ) { isPadded, paddingIfPaddedPx ->
+                            if (isPadded) {
+                                paddingIfPaddedPx
+                            } else {
+                                0
+                            }
+                        }
+                        .collect { paddingPx ->
+                            indicationArea.setPadding(paddingPx, 0, paddingPx, 0)
+                        }
+                }
+
+                launch {
+                    configurationBasedDimensions
+                        .map { it.defaultBurnInPreventionYOffsetPx }
+                        .flatMapLatest { defaultBurnInOffsetY ->
+                            viewModel.indicationAreaTranslationY(defaultBurnInOffsetY)
+                        }
+                        .collect { translationY ->
+                            indicationArea.translationY = translationY
+                            ambientIndicationArea?.translationY = translationY
+                        }
+                }
+
+                launch {
+                    configurationBasedDimensions.collect { dimensions ->
+                        indicationText.setTextSize(
+                            TypedValue.COMPLEX_UNIT_PX,
+                            dimensions.indicationTextSizePx.toFloat(),
+                        )
+                        indicationTextBottom.setTextSize(
+                            TypedValue.COMPLEX_UNIT_PX,
+                            dimensions.indicationTextSizePx.toFloat(),
+                        )
+
+                        startButton.updateLayoutParams<ViewGroup.LayoutParams> {
+                            width = dimensions.buttonSizePx.width
+                            height = dimensions.buttonSizePx.height
+                        }
+                        endButton.updateLayoutParams<ViewGroup.LayoutParams> {
+                            width = dimensions.buttonSizePx.width
+                            height = dimensions.buttonSizePx.height
+                        }
+                    }
+                }
+            }
+        }
+
+        return object : Binding {
+            override fun getIndicationAreaAnimators(): List<ViewPropertyAnimator> {
+                return listOf(indicationArea, ambientIndicationArea).mapNotNull { it?.animate() }
+            }
+
+            override fun onConfigurationChanged() {
+                configurationBasedDimensions.value = loadFromResources(view)
+            }
+        }
+    }
+
+    private fun updateButton(
+        view: ImageView,
+        viewModel: KeyguardQuickAffordanceViewModel,
+        animateReveal: Boolean,
+        falsingManager: FalsingManager,
+    ) {
+        if (!viewModel.isVisible) {
+            view.isVisible = false
+            return
+        }
+
+        if (!view.isVisible) {
+            view.isVisible = true
+            if (animateReveal) {
+                view.alpha = 0f
+                view.translationY = view.height / 2f
+                view
+                    .animate()
+                    .alpha(1f)
+                    .translationY(0f)
+                    .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
+                    .setDuration(EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS)
+                    .start()
+            }
+        }
+
+        when (viewModel.icon) {
+            is ContainedDrawable.WithDrawable -> view.setImageDrawable(viewModel.icon.drawable)
+            is ContainedDrawable.WithResource -> view.setImageResource(viewModel.icon.resourceId)
+        }
+
+        view.drawable.setTint(
+            Utils.getColorAttrDefaultColor(
+                view.context,
+                com.android.internal.R.attr.textColorPrimary
+            )
+        )
+        view.backgroundTintList =
+            Utils.getColorAttr(view.context, com.android.internal.R.attr.colorSurface)
+
+        view.contentDescription = view.context.getString(viewModel.contentDescriptionResourceId)
+        view.setOnClickListener {
+            if (falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
+                return@setOnClickListener
+            }
+
+            if (viewModel.configKey != null) {
+                viewModel.onClicked(
+                    KeyguardQuickAffordanceViewModel.OnClickedParameters(
+                        configKey = viewModel.configKey,
+                        animationController = ActivityLaunchAnimator.Controller.fromView(view),
+                    )
+                )
+            }
+        }
+    }
+
+    private fun loadFromResources(view: View): ConfigurationBasedDimensions {
+        return ConfigurationBasedDimensions(
+            defaultBurnInPreventionYOffsetPx =
+                view.resources.getDimensionPixelOffset(R.dimen.default_burn_in_prevention_offset),
+            indicationAreaPaddingPx =
+                view.resources.getDimensionPixelOffset(R.dimen.keyguard_indication_area_padding),
+            indicationTextSizePx =
+                view.resources.getDimensionPixelSize(
+                    com.android.internal.R.dimen.text_size_small_material,
+                ),
+            buttonSizePx =
+                Size(
+                    view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width),
+                    view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
+                ),
+        )
+    }
+
+    private data class ConfigurationBasedDimensions(
+        val defaultBurnInPreventionYOffsetPx: Int,
+        val indicationAreaPaddingPx: Int,
+        val indicationTextSizePx: Int,
+        val buttonSizePx: Size,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
new file mode 100644
index 0000000..4b69a81
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModel.kt
@@ -0,0 +1,114 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.keyguard.domain.usecase.ObserveAnimateBottomAreaTransitionsUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveBottomAreaAlphaUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveClockPositionUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveDozeAmountUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveIsDozingUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveKeyguardQuickAffordanceUseCase
+import com.android.systemui.keyguard.domain.usecase.OnKeyguardQuickAffordanceClickedUseCase
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+
+/** View-model for the keyguard bottom area view */
+class KeyguardBottomAreaViewModel
+@Inject
+constructor(
+    private val observeQuickAffordanceUseCase: ObserveKeyguardQuickAffordanceUseCase,
+    private val onQuickAffordanceClickedUseCase: OnKeyguardQuickAffordanceClickedUseCase,
+    observeBottomAreaAlphaUseCase: ObserveBottomAreaAlphaUseCase,
+    observeIsDozingUseCase: ObserveIsDozingUseCase,
+    observeAnimateBottomAreaTransitionsUseCase: ObserveAnimateBottomAreaTransitionsUseCase,
+    private val observeDozeAmountUseCase: ObserveDozeAmountUseCase,
+    observeClockPositionUseCase: ObserveClockPositionUseCase,
+    private val burnInHelperWrapper: BurnInHelperWrapper,
+) {
+    /** An observable for the view-model of the "start button" quick affordance. */
+    val startButton: Flow<KeyguardQuickAffordanceViewModel> =
+        button(KeyguardQuickAffordancePosition.BOTTOM_START)
+    /** An observable for the view-model of the "end button" quick affordance. */
+    val endButton: Flow<KeyguardQuickAffordanceViewModel> =
+        button(KeyguardQuickAffordancePosition.BOTTOM_END)
+    /**
+     * An observable for whether the next time a quick action button becomes visible, it should
+     * animate.
+     */
+    val animateButtonReveal: Flow<Boolean> =
+        observeAnimateBottomAreaTransitionsUseCase().distinctUntilChanged()
+    /** An observable for whether the overlay container should be visible. */
+    val isOverlayContainerVisible: Flow<Boolean> =
+        observeIsDozingUseCase().map { !it }.distinctUntilChanged()
+    /** An observable for the alpha level for the entire bottom area. */
+    val alpha: Flow<Float> = observeBottomAreaAlphaUseCase().distinctUntilChanged()
+    /** An observable for whether the indication area should be padded. */
+    val isIndicationAreaPadded: Flow<Boolean> =
+        combine(startButton, endButton) { startButtonModel, endButtonModel ->
+                startButtonModel.isVisible || endButtonModel.isVisible
+            }
+            .distinctUntilChanged()
+    /** An observable for the x-offset by which the indication area should be translated. */
+    val indicationAreaTranslationX: Flow<Float> =
+        observeClockPositionUseCase().map { it.x.toFloat() }.distinctUntilChanged()
+
+    /** Returns an observable for the y-offset by which the indication area should be translated. */
+    fun indicationAreaTranslationY(defaultBurnInOffset: Int): Flow<Float> {
+        return observeDozeAmountUseCase()
+            .map { dozeAmount ->
+                dozeAmount *
+                    (burnInHelperWrapper.burnInOffset(
+                        /* amplitude = */ defaultBurnInOffset * 2,
+                        /* xAxis= */ false,
+                    ) - defaultBurnInOffset)
+            }
+            .distinctUntilChanged()
+    }
+
+    private fun button(
+        position: KeyguardQuickAffordancePosition
+    ): Flow<KeyguardQuickAffordanceViewModel> {
+        return observeQuickAffordanceUseCase(position)
+            .map { model -> model.toViewModel() }
+            .distinctUntilChanged()
+    }
+
+    private fun KeyguardQuickAffordanceModel.toViewModel(): KeyguardQuickAffordanceViewModel {
+        return when (this) {
+            is KeyguardQuickAffordanceModel.Visible ->
+                KeyguardQuickAffordanceViewModel(
+                    configKey = configKey,
+                    isVisible = true,
+                    icon = icon,
+                    contentDescriptionResourceId = contentDescriptionResourceId,
+                    onClicked = { parameters ->
+                        onQuickAffordanceClickedUseCase(
+                            configKey = parameters.configKey,
+                            animationController = parameters.animationController,
+                        )
+                    },
+                )
+            is KeyguardQuickAffordanceModel.Hidden -> KeyguardQuickAffordanceViewModel()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
new file mode 100644
index 0000000..2417998
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceViewModel.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import androidx.annotation.StringRes
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.containeddrawable.ContainedDrawable
+import kotlin.reflect.KClass
+
+/** Models the UI state of a keyguard quick affordance button. */
+data class KeyguardQuickAffordanceViewModel(
+    val configKey: KClass<*>? = null,
+    val isVisible: Boolean = false,
+    val icon: ContainedDrawable = ContainedDrawable.WithResource(0),
+    @StringRes val contentDescriptionResourceId: Int = 0,
+    val onClicked: (OnClickedParameters) -> Unit = {},
+) {
+    data class OnClickedParameters(
+        val configKey: KClass<*>,
+        val animationController: ActivityLaunchAnimator.Controller?,
+    )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
index db446c3..dc23684d 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogBuffer.kt
@@ -20,14 +20,19 @@
 import android.util.Log
 import com.android.systemui.log.dagger.LogModule
 import com.android.systemui.util.collection.RingBuffer
+import com.google.errorprone.annotations.CompileTimeConstant
 import java.io.PrintWriter
 import java.text.SimpleDateFormat
+import java.util.Arrays.stream
 import java.util.Locale
 import java.util.concurrent.ArrayBlockingQueue
 import java.util.concurrent.BlockingQueue
 import kotlin.concurrent.thread
 import kotlin.math.max
 
+const val UNBOUNDED_STACK_TRACE = -1
+const val NESTED_TRACE_DEPTH = 10
+
 /**
  * A simple ring buffer of recyclable log messages
  *
@@ -69,12 +74,18 @@
  * @param maxSize The maximum number of messages to keep in memory at any one time. Buffers start
  * out empty and grow up to [maxSize] as new messages are logged. Once the buffer's size reaches
  * the maximum, it behaves like a ring buffer.
+ * @param rootStackTraceDepth The number of stack trace elements to be logged for an exception when
+ * the logBuffer is dumped. Defaulted to -1 [UNBOUNDED_STACK_TRACE] to print the entire stack trace.
+ * @param nestedStackTraceDepth The number of stack trace elements to be logged for any nested
+ * exceptions present in [Throwable.cause] or [Throwable.suppressedExceptions].
  */
 class LogBuffer @JvmOverloads constructor(
     private val name: String,
     private val maxSize: Int,
     private val logcatEchoTracker: LogcatEchoTracker,
-    private val systrace: Boolean = true
+    private val systrace: Boolean = true,
+    private val rootStackTraceDepth: Int = UNBOUNDED_STACK_TRACE,
+    private val nestedStackTraceDepth: Int = NESTED_TRACE_DEPTH,
 ) {
     private val buffer = RingBuffer(maxSize) { LogMessageImpl.create() }
 
@@ -107,11 +118,11 @@
      * May also log the message to logcat if echoing is enabled for this buffer or tag.
      *
      * The actual string of the log message is not constructed until it is needed. To accomplish
-     * this, logging a message is a two-step process. First, a fresh instance  of [LogMessage] is
-     * obtained and is passed to the [initializer]. The initializer stores any relevant data on the
-     * message's fields. The message is then inserted into the buffer where it waits until it is
-     * either pushed out by newer messages or it needs to printed. If and when this latter moment
-     * occurs, the [printer] function is called on the message. It reads whatever data the
+     * this, logging a message is a two-step process. First, a fresh instance of [LogMessage] is
+     * obtained and is passed to the [messageInitializer]. The initializer stores any relevant data
+     * on the message's fields. The message is then inserted into the buffer where it waits until it
+     * is either pushed out by newer messages or it needs to printed. If and when this latter moment
+     * occurs, the [messagePrinter] function is called on the message. It reads whatever data the
      * initializer stored and converts it to a human-readable log message.
      *
      * @param tag A string of at most 23 characters, used for grouping logs into categories or
@@ -120,27 +131,49 @@
      * echoed. In general, a module should split most of its logs into either INFO or DEBUG level.
      * INFO level should be reserved for information that other parts of the system might care
      * about, leaving the specifics of code's day-to-day operations to DEBUG.
-     * @param initializer A function that will be called immediately to store relevant data on the
-     * log message. The value of `this` will be the LogMessage to be initialized.
-     * @param printer A function that will be called if and when the message needs to be dumped to
-     * logcat or a bug report. It should read the data stored by the initializer and convert it to
-     * a human-readable string. The value of `this` will be the LogMessage to be printed.
-     * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any
-     * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance
-     * of the printer for each call, thwarting our attempts at avoiding any sort of allocation.
+     * @param messageInitializer A function that will be called immediately to store relevant data
+     * on the log message. The value of `this` will be the LogMessage to be initialized.
+     * @param messagePrinter A function that will be called if and when the message needs to be
+     * dumped to logcat or a bug report. It should read the data stored by the initializer and
+     * convert it to a human-readable string. The value of `this` will be the LogMessage to be
+     * printed. **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and
+     * NEVER any variables in its enclosing scope. Otherwise, the runtime will need to allocate a
+     * new instance of the printer for each call, thwarting our attempts at avoiding any sort of
+     * allocation.
+     * @param exception Provide any exception that need to be logged. This is saved as
+     * [LogMessage.exception]
      */
+    @JvmOverloads
     inline fun log(
-        tag: String,
-        level: LogLevel,
-        initializer: LogMessage.() -> Unit,
-        noinline printer: LogMessage.() -> String
+            tag: String,
+            level: LogLevel,
+            messageInitializer: MessageInitializer,
+            noinline messagePrinter: MessagePrinter,
+            exception: Throwable? = null,
     ) {
-        val message = obtain(tag, level, printer)
-        initializer(message)
+        val message = obtain(tag, level, messagePrinter, exception)
+        messageInitializer(message)
         commit(message)
     }
 
     /**
+     * Logs a compile-time string constant [message] to the log buffer. Use sparingly.
+     *
+     * May also log the message to logcat if echoing is enabled for this buffer or tag. This is for
+     * simpler use-cases where [message] is a compile time string constant. For use-cases where the
+     * log message is built during runtime, use the [LogBuffer.log] overloaded method that takes in
+     * an initializer and a message printer.
+     *
+     * Log buffers are limited by the number of entries, so logging more frequently
+     * will limit the time window that the LogBuffer covers in a bug report.  Richer logs, on the
+     * other hand, make a bug report more actionable, so using the [log] with a messagePrinter to
+     * add more detail to every log may do more to improve overall logging than adding more logs
+     * with this method.
+     */
+    fun log(tag: String, level: LogLevel, @CompileTimeConstant message: String) =
+            log(tag, level, {str1 = message}, { str1!! })
+
+    /**
      * You should call [log] instead of this method.
      *
      * Obtains the next [LogMessage] from the ring buffer. If the buffer is not yet at max size,
@@ -151,15 +184,16 @@
      */
     @Synchronized
     fun obtain(
-        tag: String,
-        level: LogLevel,
-        printer: (LogMessage) -> String
-    ): LogMessageImpl {
+            tag: String,
+            level: LogLevel,
+            messagePrinter: MessagePrinter,
+            exception: Throwable? = null,
+    ): LogMessage {
         if (!mutable) {
             return FROZEN_MESSAGE
         }
         val message = buffer.advance()
-        message.reset(tag, level, System.currentTimeMillis(), printer)
+        message.reset(tag, level, System.currentTimeMillis(), messagePrinter, exception)
         return message
     }
 
@@ -230,19 +264,79 @@
         }
     }
 
-    private fun dumpMessage(message: LogMessage, pw: PrintWriter) {
-        pw.print(DATE_FORMAT.format(message.timestamp))
+    private fun dumpMessage(
+        message: LogMessage,
+        pw: PrintWriter
+    ) {
+        val formattedTimestamp = DATE_FORMAT.format(message.timestamp)
+        val shortLevel = message.level.shortString
+        val messageToPrint = message.messagePrinter(message)
+        val tag = message.tag
+        printLikeLogcat(pw, formattedTimestamp, shortLevel, tag, messageToPrint)
+        message.exception?.let { ex ->
+            printException(
+                pw,
+                formattedTimestamp,
+                shortLevel,
+                ex,
+                tag,
+                stackTraceDepth = rootStackTraceDepth)
+        }
+    }
+
+    private fun printException(
+            pw: PrintWriter,
+            timestamp: String,
+            level: String,
+            exception: Throwable,
+            tag: String,
+            exceptionMessagePrefix: String = "",
+            stackTraceDepth: Int = UNBOUNDED_STACK_TRACE
+    ) {
+        val message = "$exceptionMessagePrefix$exception"
+        printLikeLogcat(pw, timestamp, level, tag, message)
+        var stacktraceStream = stream(exception.stackTrace)
+        if (stackTraceDepth != UNBOUNDED_STACK_TRACE) {
+            stacktraceStream = stacktraceStream.limit(stackTraceDepth.toLong())
+        }
+        stacktraceStream.forEach { line ->
+            printLikeLogcat(pw, timestamp, level, tag, "\tat $line")
+        }
+        exception.cause?.let { cause ->
+            printException(pw, timestamp, level, cause, tag, "Caused by: ", nestedStackTraceDepth)
+        }
+        exception.suppressedExceptions.forEach { suppressed ->
+            printException(
+                pw,
+                timestamp,
+                level,
+                suppressed,
+                tag,
+                "Suppressed: ",
+                nestedStackTraceDepth
+            )
+        }
+    }
+
+    private fun printLikeLogcat(
+        pw: PrintWriter,
+        formattedTimestamp: String,
+        shortLogLevel: String,
+        tag: String,
+        message: String
+    ) {
+        pw.print(formattedTimestamp)
         pw.print(" ")
-        pw.print(message.level.shortString)
+        pw.print(shortLogLevel)
         pw.print(" ")
-        pw.print(message.tag)
+        pw.print(tag)
         pw.print(": ")
-        pw.println(message.printer(message))
+        pw.println(message)
     }
 
     private fun echo(message: LogMessage, toLogcat: Boolean, toSystrace: Boolean) {
         if (toLogcat || toSystrace) {
-            val strMessage = message.printer(message)
+            val strMessage = message.messagePrinter(message)
             if (toSystrace) {
                 echoToSystrace(message, strMessage)
             }
@@ -259,16 +353,22 @@
 
     private fun echoToLogcat(message: LogMessage, strMessage: String) {
         when (message.level) {
-            LogLevel.VERBOSE -> Log.v(message.tag, strMessage)
-            LogLevel.DEBUG -> Log.d(message.tag, strMessage)
-            LogLevel.INFO -> Log.i(message.tag, strMessage)
-            LogLevel.WARNING -> Log.w(message.tag, strMessage)
-            LogLevel.ERROR -> Log.e(message.tag, strMessage)
-            LogLevel.WTF -> Log.wtf(message.tag, strMessage)
+            LogLevel.VERBOSE -> Log.v(message.tag, strMessage, message.exception)
+            LogLevel.DEBUG -> Log.d(message.tag, strMessage, message.exception)
+            LogLevel.INFO -> Log.i(message.tag, strMessage, message.exception)
+            LogLevel.WARNING -> Log.w(message.tag, strMessage, message.exception)
+            LogLevel.ERROR -> Log.e(message.tag, strMessage, message.exception)
+            LogLevel.WTF -> Log.wtf(message.tag, strMessage, message.exception)
         }
     }
 }
 
+/**
+ * A function that will be called immediately to store relevant data on the log message. The value
+ * of `this` will be the LogMessage to be initialized.
+ */
+typealias MessageInitializer = LogMessage.() -> Unit
+
 private const val TAG = "LogBuffer"
 private val DATE_FORMAT = SimpleDateFormat("MM-dd HH:mm:ss.SSS", Locale.US)
-private val FROZEN_MESSAGE = LogMessageImpl.create()
\ No newline at end of file
+private val FROZEN_MESSAGE = LogMessageImpl.create()
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt b/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
index 2a0a2aa6..987aea8 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogMessage.kt
@@ -25,7 +25,7 @@
  *
  * When a message is logged, the code doing the logging stores data in one or more of the generic
  * fields ([str1], [int1], etc). When it comes time to dump the message to logcat/bugreport/etc, the
- * [printer] function reads the data stored in the generic fields and converts that to a human-
+ * [messagePrinter] function reads the data stored in the generic fields and converts that to a human-
  * readable string. Thus, for every log type there must be a specialized initializer function that
  * stores data specific to that log type and a specialized printer function that prints that data.
  *
@@ -35,7 +35,8 @@
     val level: LogLevel
     val tag: String
     val timestamp: Long
-    val printer: LogMessage.() -> String
+    val messagePrinter: MessagePrinter
+    val exception: Throwable?
 
     var str1: String?
     var str2: String?
@@ -50,3 +51,13 @@
     var bool3: Boolean
     var bool4: Boolean
 }
+
+/**
+ * A function that will be called if and when the message needs to be dumped to
+ * logcat or a bug report. It should read the data stored by the initializer and convert it to
+ * a human-readable string. The value of `this` will be the LogMessage to be printed.
+ * **IMPORTANT:** The printer should ONLY ever reference fields on the LogMessage and NEVER any
+ * variables in its enclosing scope. Otherwise, the runtime will need to allocate a new instance
+ * of the printer for each call, thwarting our attempts at avoiding any sort of allocation.
+ */
+typealias MessagePrinter = LogMessage.() -> String
diff --git a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt b/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
index d33ac4b..4dd6f65 100644
--- a/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/LogMessageImpl.kt
@@ -23,7 +23,8 @@
     override var level: LogLevel,
     override var tag: String,
     override var timestamp: Long,
-    override var printer: LogMessage.() -> String,
+    override var messagePrinter: MessagePrinter,
+    override var exception: Throwable?,
     override var str1: String?,
     override var str2: String?,
     override var str3: String?,
@@ -35,19 +36,21 @@
     override var bool1: Boolean,
     override var bool2: Boolean,
     override var bool3: Boolean,
-    override var bool4: Boolean
+    override var bool4: Boolean,
 ) : LogMessage {
 
     fun reset(
         tag: String,
         level: LogLevel,
         timestamp: Long,
-        renderer: LogMessage.() -> String
+        renderer: MessagePrinter,
+        exception: Throwable? = null,
     ) {
         this.level = level
         this.tag = tag
         this.timestamp = timestamp
-        this.printer = renderer
+        this.messagePrinter = renderer
+        this.exception = exception
         str1 = null
         str2 = null
         str3 = null
@@ -68,7 +71,8 @@
                     LogLevel.DEBUG,
                     DEFAULT_TAG,
                     0,
-                    DEFAULT_RENDERER,
+                    DEFAULT_PRINTER,
+                    null,
                     null,
                     null,
                     null,
@@ -86,4 +90,4 @@
 }
 
 private const val DEFAULT_TAG = "UnknownTag"
-private val DEFAULT_RENDERER: LogMessage.() -> String = { "Unknown message: $this" }
+private val DEFAULT_PRINTER: MessagePrinter = { "Unknown message: $this" }
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardUpdateMonitorLog.kt b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardUpdateMonitorLog.kt
new file mode 100644
index 0000000..323ee21
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/KeyguardUpdateMonitorLog.kt
@@ -0,0 +1,4 @@
+package com.android.systemui.log.dagger
+
+/** A [com.android.systemui.log.LogBuffer] for KeyguardUpdateMonitor. */
+annotation class KeyguardUpdateMonitorLog
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
index d0da18a..c858bc3 100644
--- a/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/LogModule.java
@@ -255,6 +255,16 @@
         return factory.create("MediaCarouselCtlrLog", 20);
     }
 
+    /**
+     * Provides a {@link LogBuffer} for use in the status bar connectivity pipeline
+     */
+    @Provides
+    @SysUISingleton
+    @StatusBarConnectivityLog
+    public static LogBuffer provideStatusBarConnectivityBuffer(LogBufferFactory factory) {
+        return factory.create("StatusBarConnectivityLog", 64);
+    }
+
     /** Allows logging buffers to be tweaked via adb on debug builds but not on prod builds. */
     @Provides
     @SysUISingleton
@@ -277,4 +287,14 @@
     public static LogBuffer provideStatusBarNetworkControllerBuffer(LogBufferFactory factory) {
         return factory.create("StatusBarNetworkControllerLog", 20);
     }
+
+    /**
+     * Provides a {@link LogBuffer} for use by {@link com.android.keyguard.KeyguardUpdateMonitor}.
+     */
+    @Provides
+    @SysUISingleton
+    @KeyguardUpdateMonitorLog
+    public static LogBuffer provideKeyguardUpdateMonitorLogBuffer(LogBufferFactory factory) {
+        return factory.create("KeyguardUpdateMonitorLog", 200);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
new file mode 100644
index 0000000..f03fbcb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/log/dagger/StatusBarConnectivityLog.java
@@ -0,0 +1,36 @@
+/*
+ * 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.log.dagger;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.android.systemui.log.LogBuffer;
+import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+
+import javax.inject.Qualifier;
+
+/**
+ * A {@link LogBuffer} for events processed by {@link ConnectivityInfoProcessor}
+ */
+@Qualifier
+@Documented
+@Retention(RUNTIME)
+public @interface StatusBarConnectivityLog {
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt
index e95976f..a29c588 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/ChipInfoCommon.kt
@@ -27,4 +27,4 @@
     fun getTimeoutMs(): Long
 }
 
-const val DEFAULT_TIMEOUT_MILLIS = 4000L
+const val DEFAULT_TIMEOUT_MILLIS = 10000L
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
index c9fce79..5f478ce 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommon.kt
@@ -26,18 +26,18 @@
 import android.os.SystemClock
 import android.util.Log
 import android.view.LayoutInflater
-import android.view.MotionEvent
 import android.view.ViewGroup
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS
 import android.view.accessibility.AccessibilityManager.FLAG_CONTENT_TEXT
+import androidx.annotation.CallSuper
 import com.android.internal.widget.CachingIconView
 import com.android.settingslib.Utils
 import com.android.systemui.R
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.statusbar.gesture.TapGestureDetector
+import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
 
@@ -52,17 +52,16 @@
  * display the chip in a certain state, since they receive <T> in [updateChipView].
  */
 abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>(
-    internal val context: Context,
-    internal val logger: MediaTttLogger,
-    internal val windowManager: WindowManager,
-    private val viewUtil: ViewUtil,
-    @Main private val mainExecutor: DelayableExecutor,
-    private val accessibilityManager: AccessibilityManager,
-    private val tapGestureDetector: TapGestureDetector,
-    private val powerManager: PowerManager,
-    @LayoutRes private val chipLayoutRes: Int
+        internal val context: Context,
+        internal val logger: MediaTttLogger,
+        internal val windowManager: WindowManager,
+        private val viewUtil: ViewUtil,
+        @Main private val mainExecutor: DelayableExecutor,
+        private val accessibilityManager: AccessibilityManager,
+        private val configurationController: ConfigurationController,
+        private val powerManager: PowerManager,
+        @LayoutRes private val chipLayoutRes: Int,
 ) {
-
     /**
      * Window layout params that will be used as a starting point for the [windowLayoutParams] of
      * all subclasses.
@@ -89,42 +88,43 @@
     /** The chip view currently being displayed. Null if the chip is not being displayed. */
     private var chipView: ViewGroup? = null
 
+    /** The chip info currently being displayed. Null if the chip is not being displayed. */
+    internal var chipInfo: T? = null
+
     /** A [Runnable] that, when run, will cancel the pending timeout of the chip. */
     private var cancelChipViewTimeout: Runnable? = null
 
     /**
-     * Displays the chip with the current state.
+     * Displays the chip with the provided [newChipInfo].
      *
      * This method handles inflating and attaching the view, then delegates to [updateChipView] to
      * display the correct information in the chip.
      */
-    fun displayChip(chipInfo: T) {
-        val oldChipView = chipView
-        if (chipView == null) {
-            chipView = LayoutInflater
-                .from(context)
-                .inflate(chipLayoutRes, null) as ViewGroup
-        }
-        val currentChipView = chipView!!
+    fun displayChip(newChipInfo: T) {
+        val currentChipView = chipView
 
-        updateChipView(chipInfo, currentChipView)
+        if (currentChipView != null) {
+            updateChipView(newChipInfo, currentChipView)
+        } else {
+            // The chip is new, so set up all our callbacks and inflate the view
+            configurationController.addCallback(displayScaleListener)
+            // Wake the screen if necessary so the user will see the chip. (Per b/239426653, we want
+            // the chip to show over the dream state, so we should only wake up if the screen is
+            // completely off.)
+            if (!powerManager.isScreenOn) {
+                powerManager.wakeUp(
+                        SystemClock.uptimeMillis(),
+                        PowerManager.WAKE_REASON_APPLICATION,
+                        "com.android.systemui:media_tap_to_transfer_activated"
+                )
+            }
 
-        // Add view if necessary
-        if (oldChipView == null) {
-            tapGestureDetector.addOnGestureDetectedCallback(TAG, this::onScreenTapped)
-            windowManager.addView(chipView, windowLayoutParams)
-            // Wake the screen so the user will see the chip
-            powerManager.wakeUp(
-                SystemClock.uptimeMillis(),
-                PowerManager.WAKE_REASON_APPLICATION,
-                "com.android.systemui:media_tap_to_transfer_activated"
-            )
-            animateChipIn(currentChipView)
+            inflateAndUpdateChip(newChipInfo)
         }
 
         // Cancel and re-set the chip timeout each time we get a new state.
         val timeout = accessibilityManager.getRecommendedTimeoutMillis(
-            chipInfo.getTimeoutMs().toInt(),
+            newChipInfo.getTimeoutMs().toInt(),
             // Not all chips have controls so FLAG_CONTENT_CONTROLS might be superfluous, but
             // include it just to be safe.
             FLAG_CONTENT_ICONS or FLAG_CONTENT_TEXT or FLAG_CONTENT_CONTROLS
@@ -136,6 +136,32 @@
         )
     }
 
+    /** Inflates a new chip view, updates it with [newChipInfo], and adds the view to the window. */
+    private fun inflateAndUpdateChip(newChipInfo: T) {
+        val newChipView = LayoutInflater
+                .from(context)
+                .inflate(chipLayoutRes, null) as ViewGroup
+        chipView = newChipView
+        updateChipView(newChipInfo, newChipView)
+        windowManager.addView(newChipView, windowLayoutParams)
+        animateChipIn(newChipView)
+    }
+
+    /** Removes then re-inflates the chip. */
+    private fun reinflateChip() {
+        val currentChipInfo = chipInfo
+        if (chipView == null || currentChipInfo == null) { return }
+
+        windowManager.removeView(chipView)
+        inflateAndUpdateChip(currentChipInfo)
+    }
+
+    private val displayScaleListener = object : ConfigurationController.ConfigurationListener {
+        override fun onDensityOrFontScaleChanged() {
+            reinflateChip()
+        }
+    }
+
     /**
      * Hides the chip.
      *
@@ -145,17 +171,21 @@
     open fun removeChip(removalReason: String) {
         if (chipView == null) { return }
         logger.logChipRemoval(removalReason)
-        tapGestureDetector.removeOnGestureDetectedCallback(TAG)
+        configurationController.removeCallback(displayScaleListener)
         windowManager.removeView(chipView)
         chipView = null
+        chipInfo = null
         // No need to time the chip out since it's already gone
         cancelChipViewTimeout?.run()
     }
 
     /**
-     * A method implemented by subclasses to update [currentChipView] based on [chipInfo].
+     * A method implemented by subclasses to update [currentChipView] based on [newChipInfo].
      */
-    abstract fun updateChipView(chipInfo: T, currentChipView: ViewGroup)
+    @CallSuper
+    open fun updateChipView(newChipInfo: T, currentChipView: ViewGroup) {
+        chipInfo = newChipInfo
+    }
 
     /**
      * A method that can be implemented by subclcasses to do custom animations for when the chip
@@ -226,15 +256,6 @@
             isAppIcon = false
         )
     }
-
-    private fun onScreenTapped(e: MotionEvent) {
-        val view = chipView ?: return
-        // If the tap is within the chip bounds, we shouldn't hide the chip (in case users think the
-        // chip is tappable).
-        if (!viewUtil.touchIsWithinView(view, e.x, e.y)) {
-            removeChip(MediaTttRemovalReason.REASON_SCREEN_TAP)
-        }
-    }
 }
 
 // Used in CTS tests UpdateMediaTapToTransferSenderDisplayTest and
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 99a5b8b..495f697 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -41,7 +41,7 @@
 import com.android.systemui.media.taptotransfer.common.MediaTttChipControllerCommon
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.statusbar.gesture.TapGestureDetector
+import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.animation.AnimationUtil.Companion.frames
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
@@ -54,27 +54,27 @@
  */
 @SysUISingleton
 class MediaTttChipControllerReceiver @Inject constructor(
-    commandQueue: CommandQueue,
-    context: Context,
-    @MediaTttReceiverLogger logger: MediaTttLogger,
-    windowManager: WindowManager,
-    viewUtil: ViewUtil,
-    mainExecutor: DelayableExecutor,
-    accessibilityManager: AccessibilityManager,
-    tapGestureDetector: TapGestureDetector,
-    powerManager: PowerManager,
-    @Main private val mainHandler: Handler,
-    private val uiEventLogger: MediaTttReceiverUiEventLogger,
+        commandQueue: CommandQueue,
+        context: Context,
+        @MediaTttReceiverLogger logger: MediaTttLogger,
+        windowManager: WindowManager,
+        viewUtil: ViewUtil,
+        mainExecutor: DelayableExecutor,
+        accessibilityManager: AccessibilityManager,
+        configurationController: ConfigurationController,
+        powerManager: PowerManager,
+        @Main private val mainHandler: Handler,
+        private val uiEventLogger: MediaTttReceiverUiEventLogger,
 ) : MediaTttChipControllerCommon<ChipReceiverInfo>(
-    context,
-    logger,
-    windowManager,
-    viewUtil,
-    mainExecutor,
-    accessibilityManager,
-    tapGestureDetector,
-    powerManager,
-    R.layout.media_ttt_chip_receiver
+        context,
+        logger,
+        windowManager,
+        viewUtil,
+        mainExecutor,
+        accessibilityManager,
+        configurationController,
+        powerManager,
+        R.layout.media_ttt_chip_receiver,
 ) {
     @SuppressLint("WrongConstant") // We're allowed to use LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
     override val windowLayoutParams = commonWindowLayoutParams.apply {
@@ -140,12 +140,13 @@
         )
     }
 
-    override fun updateChipView(chipInfo: ChipReceiverInfo, currentChipView: ViewGroup) {
+    override fun updateChipView(newChipInfo: ChipReceiverInfo, currentChipView: ViewGroup) {
+        super.updateChipView(newChipInfo, currentChipView)
         setIcon(
                 currentChipView,
-                chipInfo.routeInfo.packageName,
-                chipInfo.appIconDrawableOverride,
-                chipInfo.appNameOverride
+                newChipInfo.routeInfo.packageName,
+                newChipInfo.appIconDrawableOverride,
+                newChipInfo.appNameOverride
         )
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
index cd86fff..a153cb6 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/ChipStateSender.kt
@@ -243,6 +243,6 @@
 // Give the Transfer*Triggered states a longer timeout since those states represent an active
 // process and we should keep the user informed about it as long as possible (but don't allow it to
 // continue indefinitely).
-private const val TRANSFER_TRIGGERED_TIMEOUT_MILLIS = 15000L
+private const val TRANSFER_TRIGGERED_TIMEOUT_MILLIS = 30000L
 
 private const val TAG = "ChipStateSender"
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
index 797a770..3ea11b8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSender.kt
@@ -38,7 +38,7 @@
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.media.taptotransfer.common.MediaTttRemovalReason
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.statusbar.gesture.TapGestureDetector
+import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.view.ViewUtil
 import javax.inject.Inject
@@ -49,33 +49,31 @@
  */
 @SysUISingleton
 class MediaTttChipControllerSender @Inject constructor(
-    commandQueue: CommandQueue,
-    context: Context,
-    @MediaTttSenderLogger logger: MediaTttLogger,
-    windowManager: WindowManager,
-    viewUtil: ViewUtil,
-    @Main mainExecutor: DelayableExecutor,
-    accessibilityManager: AccessibilityManager,
-    tapGestureDetector: TapGestureDetector,
-    powerManager: PowerManager,
-    private val uiEventLogger: MediaTttSenderUiEventLogger
+        commandQueue: CommandQueue,
+        context: Context,
+        @MediaTttSenderLogger logger: MediaTttLogger,
+        windowManager: WindowManager,
+        viewUtil: ViewUtil,
+        @Main mainExecutor: DelayableExecutor,
+        accessibilityManager: AccessibilityManager,
+        configurationController: ConfigurationController,
+        powerManager: PowerManager,
+        private val uiEventLogger: MediaTttSenderUiEventLogger
 ) : MediaTttChipControllerCommon<ChipSenderInfo>(
-    context,
-    logger,
-    windowManager,
-    viewUtil,
-    mainExecutor,
-    accessibilityManager,
-    tapGestureDetector,
-    powerManager,
-    R.layout.media_ttt_chip
+        context,
+        logger,
+        windowManager,
+        viewUtil,
+        mainExecutor,
+        accessibilityManager,
+        configurationController,
+        powerManager,
+        R.layout.media_ttt_chip,
 ) {
     override val windowLayoutParams = commonWindowLayoutParams.apply {
         gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL)
     }
 
-    private var currentlyDisplayedChipState: ChipStateSender? = null
-
     private val commandQueueCallbacks = object : CommandQueue.Callbacks {
         override fun updateMediaTapToTransferSenderDisplay(
                 @StatusBarManager.MediaTransferSenderState displayState: Int,
@@ -116,16 +114,18 @@
 
     /** Displays the chip view for the given state. */
     override fun updateChipView(
-            chipInfo: ChipSenderInfo,
-            currentChipView: ViewGroup) {
-        val chipState = chipInfo.state
-        currentlyDisplayedChipState = chipState
+            newChipInfo: ChipSenderInfo,
+            currentChipView: ViewGroup
+    ) {
+        super.updateChipView(newChipInfo, currentChipView)
+
+        val chipState = newChipInfo.state
 
         // App icon
-        setIcon(currentChipView, chipInfo.routeInfo.packageName)
+        setIcon(currentChipView, newChipInfo.routeInfo.packageName)
 
         // Text
-        val otherDeviceName = chipInfo.routeInfo.name.toString()
+        val otherDeviceName = newChipInfo.routeInfo.name.toString()
         currentChipView.requireViewById<TextView>(R.id.text).apply {
             text = chipState.getChipTextString(context, otherDeviceName)
         }
@@ -137,7 +137,7 @@
         // Undo
         val undoView = currentChipView.requireViewById<View>(R.id.undo)
         val undoClickListener = chipState.undoClickListener(
-                this, chipInfo.routeInfo, chipInfo.undoCallback, uiEventLogger
+                this, newChipInfo.routeInfo, newChipInfo.undoCallback, uiEventLogger
         )
         undoView.setOnClickListener(undoClickListener)
         undoView.visibility = (undoClickListener != null).visibleIfTrue()
@@ -161,12 +161,11 @@
     override fun removeChip(removalReason: String) {
         // Don't remove the chip if we're mid-transfer since the user should still be able to
         // see the status of the transfer. (But do remove it if it's finally timed out.)
-        if (currentlyDisplayedChipState?.isMidTransfer == true
-                && removalReason != MediaTttRemovalReason.REASON_TIMEOUT) {
+        if (chipInfo?.state?.isMidTransfer == true &&
+                removalReason != MediaTttRemovalReason.REASON_TIMEOUT) {
             return
         }
         super.removeChip(removalReason)
-        currentlyDisplayedChipState = null
     }
 
     private fun Boolean.visibleIfTrue(): Int {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
index 2a7c871..2d7a809 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarController.java
@@ -51,6 +51,8 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.shared.system.QuickStepContract;
@@ -82,6 +84,7 @@
     private final Context mContext;
     private final Handler mHandler;
     private final NavigationBarComponent.Factory mNavigationBarComponentFactory;
+    private FeatureFlags mFeatureFlags;
     private final DisplayManager mDisplayManager;
     private final TaskbarDelegate mTaskbarDelegate;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
@@ -113,10 +116,12 @@
             AutoHideController autoHideController,
             LightBarController lightBarController,
             Optional<Pip> pipOptional,
-            Optional<BackAnimation> backAnimation) {
+            Optional<BackAnimation> backAnimation,
+            FeatureFlags featureFlags) {
         mContext = context;
         mHandler = mainHandler;
         mNavigationBarComponentFactory = navigationBarComponentFactory;
+        mFeatureFlags = featureFlags;
         mDisplayManager = mContext.getSystemService(DisplayManager.class);
         commandQueue.addCallback(this);
         configurationController.addCallback(this);
@@ -217,7 +222,10 @@
 
     /** @return {@code true} if taskbar is enabled, false otherwise */
     private boolean initializeTaskbarIfNecessary() {
-        if (mIsTablet) {
+        // Enable for tablet or (phone AND flag is set); assuming phone = !mIsTablet
+        boolean taskbarEnabled = mIsTablet || mFeatureFlags.isEnabled(Flags.HIDE_NAVBAR_WINDOW);
+
+        if (taskbarEnabled) {
             Trace.beginSection("NavigationBarController#initializeTaskbarIfNecessary");
             // Remove navigation bar when taskbar is showing
             removeNavigationBar(mContext.getDisplayId());
@@ -226,7 +234,7 @@
         } else {
             mTaskbarDelegate.destroy();
         }
-        return mIsTablet;
+        return taskbarEnabled;
     }
 
     @Override
@@ -294,6 +302,10 @@
      */
     @VisibleForTesting
     void createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result) {
+        if (initializeTaskbarIfNecessary()) {
+            return;
+        }
+
         if (display == null) {
             return;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java
index 6908e5a..209d09d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/carrier/QSCarrierGroupController.java
@@ -243,7 +243,7 @@
             mActivityStarter.postStartActivityDismissingKeyguard(
                     new Intent(Settings.ACTION_WIRELESS_SETTINGS), 0);
         };
-        view.setOnClickListener(onClickListener);
+
         mNoSimTextView = view.getNoSimTextView();
         mNoSimTextView.setOnClickListener(onClickListener);
         mMainHandler = new H(mainLooper, this::handleUpdateCarrierInfo, this::handleUpdateState);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 99d0fe9..4e74540f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -128,6 +128,10 @@
 import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.usecase.SetClockPositionUseCase;
+import com.android.systemui.keyguard.domain.usecase.SetKeyguardBottomAreaAlphaUseCase;
+import com.android.systemui.keyguard.domain.usecase.SetKeyguardBottomAreaAnimateDozingTransitionsUseCase;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.MediaHierarchyManager;
@@ -290,11 +294,6 @@
     private final NotificationStackScrollLayoutController mNotificationStackScrollLayoutController;
     private final NotificationIconAreaController mNotificationIconAreaController;
 
-    // Cap and total height of Roboto font. Needs to be adjusted when font for the big clock is
-    // changed.
-    private static final int CAP_HEIGHT = 1456;
-    private static final int FONT_HEIGHT = 2163;
-
     /**
      * Maximum time before which we will expand the panel even for slow motions when getting a
      * touch passed over from launcher.
@@ -702,6 +701,12 @@
     };
 
     private final CameraGestureHelper mCameraGestureHelper;
+    private final Provider<KeyguardBottomAreaViewModel> mKeyguardBottomAreaViewModelProvider;
+    private final Provider<SetClockPositionUseCase> mSetClockPositionUseCaseProvider;
+    private final Provider<SetKeyguardBottomAreaAlphaUseCase>
+            mSetKeyguardBottomAreaAlphaUseCaseProvider;
+    private final Provider<SetKeyguardBottomAreaAnimateDozingTransitionsUseCase>
+            mSetKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider;
 
     @Inject
     public NotificationPanelViewController(NotificationPanelView view,
@@ -772,7 +777,12 @@
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
             ShadeTransitionController shadeTransitionController,
             SystemClock systemClock,
-            CameraGestureHelper cameraGestureHelper) {
+            CameraGestureHelper cameraGestureHelper,
+            Provider<KeyguardBottomAreaViewModel> keyguardBottomAreaViewModelProvider,
+            Provider<SetClockPositionUseCase> setClockPositionUseCaseProvider,
+            Provider<SetKeyguardBottomAreaAlphaUseCase> setKeyguardBottomAreaAlphaUseCaseProvider,
+            Provider<SetKeyguardBottomAreaAnimateDozingTransitionsUseCase>
+                    setKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider) {
         super(view,
                 falsingManager,
                 dozeLog,
@@ -902,6 +912,7 @@
 
         mQsFrameTranslateController = qsFrameTranslateController;
         updateUserSwitcherFlags();
+        mKeyguardBottomAreaViewModelProvider = keyguardBottomAreaViewModelProvider;
         onFinishInflate();
         keyguardUnlockAnimationController.addKeyguardUnlockAnimationListener(
                 new KeyguardUnlockAnimationController.KeyguardUnlockAnimationListener() {
@@ -955,6 +966,10 @@
                     }
                 });
         mCameraGestureHelper = cameraGestureHelper;
+        mSetClockPositionUseCaseProvider = setClockPositionUseCaseProvider;
+        mSetKeyguardBottomAreaAlphaUseCaseProvider = setKeyguardBottomAreaAlphaUseCaseProvider;
+        mSetKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider =
+                setKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider;
     }
 
     @VisibleForTesting
@@ -1107,13 +1122,6 @@
         }
     }
 
-    /**
-     * Returns if there's a custom clock being presented.
-     */
-    public boolean hasCustomClock() {
-        return mKeyguardStatusViewController.hasCustomClock();
-    }
-
     private void setCentralSurfaces(CentralSurfaces centralSurfaces) {
         // TODO: this can be injected.
         mCentralSurfaces = centralSurfaces;
@@ -1286,11 +1294,17 @@
     }
 
     private void initBottomArea() {
-        mKeyguardBottomArea.init(
-                mFalsingManager,
-                mQuickAccessWalletController,
-                mControlsComponent,
-                mQRCodeScannerController);
+        if (mFeatureFlags.isEnabled(Flags.MODERN_BOTTOM_AREA)) {
+            mKeyguardBottomArea.init(mKeyguardBottomAreaViewModelProvider.get(), mFalsingManager);
+        } else {
+            // TODO(b/235403546): remove this method call when the new implementation is complete
+            //  and these are not needed.
+            mKeyguardBottomArea.init(
+                    mFalsingManager,
+                    mQuickAccessWalletController,
+                    mControlsComponent,
+                    mQRCodeScannerController);
+        }
     }
 
     @VisibleForTesting
@@ -1466,6 +1480,8 @@
                 mKeyguardStatusViewController.getClockBottom(mStatusBarHeaderHeightKeyguard),
                 mKeyguardStatusViewController.isClockTopAligned());
         mClockPositionAlgorithm.run(mClockPositionResult);
+        mSetClockPositionUseCaseProvider.get().invoke(
+                mClockPositionResult.clockX, mClockPositionResult.clockY);
         boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending();
         boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange;
         mKeyguardStatusViewController.updatePosition(
@@ -3232,6 +3248,7 @@
         float alpha = Math.min(expansionAlpha, 1 - computeQsExpansionFraction());
         alpha *= mBottomAreaShadeAlpha;
         mKeyguardBottomArea.setComponentAlphas(alpha);
+        mSetKeyguardBottomAreaAlphaUseCaseProvider.get().invoke(alpha);
         mLockIconViewController.setAlpha(alpha);
     }
 
@@ -3431,6 +3448,7 @@
 
     private void updateDozingVisibilities(boolean animate) {
         mKeyguardBottomArea.setDozing(mDozing, animate);
+        mSetKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider.get().invoke(animate);
         if (!mDozing && animate) {
             mKeyguardStatusBarViewController.animateKeyguardStatusBarIn();
         }
@@ -3733,6 +3751,7 @@
         mDozing = dozing;
         mNotificationStackScrollLayoutController.setDozing(mDozing, animate, wakeUpTouchLocation);
         mKeyguardBottomArea.setDozing(mDozing, animate);
+        mSetKeyguardBottomAreaAnimateDozingTransitionsUseCaseProvider.get().invoke(animate);
         mKeyguardStatusBarViewController.setDozing(mDozing);
 
         if (dozing) {
@@ -3881,9 +3900,10 @@
                         endAction.run();
                     }
                 })
+                .setUpdateListener(anim -> {
+                    mKeyguardStatusViewController.animateFoldToAod(anim.getAnimatedFraction());
+                })
                 .start();
-
-        mKeyguardStatusViewController.animateFoldToAod();
     }
 
     /**
@@ -4596,7 +4616,6 @@
         public void onDozeAmountChanged(float linearAmount, float amount) {
             mInterpolatedDarkAmount = amount;
             mLinearDarkAmount = linearAmount;
-            mKeyguardStatusViewController.setDarkAmount(mInterpolatedDarkAmount);
             mKeyguardBottomArea.setDarkAmount(mInterpolatedDarkAmount);
             positionClockAndNotifications();
         }
@@ -4706,11 +4725,8 @@
             updateMaxDisplayedNotifications(!shouldAvoidChangingNotificationsCount());
             setIsFullWidth(mNotificationStackScrollLayoutController.getWidth() == mView.getWidth());
 
-            // Update Clock Pivot
-            mKeyguardStatusViewController.setPivotX(((float) mView.getWidth()) / 2f);
-            mKeyguardStatusViewController.setPivotY(
-                    (FONT_HEIGHT - CAP_HEIGHT) / 2048f
-                            * mKeyguardStatusViewController.getClockTextSize());
+            // Update Clock Pivot (used by anti-burnin transformations)
+            mKeyguardStatusViewController.updatePivot(mView.getWidth(), mView.getHeight());
 
             // Calculate quick setting heights.
             int oldMaxHeight = mQsMaxExpansionHeight;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java
index 3b3b5a2..cb414ba 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationPresenter.java
@@ -50,16 +50,6 @@
     boolean isDeviceInVrMode();
 
     /**
-     * Updates the visual representation of the notifications.
-     */
-    void updateNotificationViews(String reason);
-
-    /**
-     * Called when the row states are updated by {@link NotificationViewHierarchyManager}.
-     */
-    void onUpdateRowStates();
-
-    /**
      * @return true if the shade is collapsing.
      */
     boolean isCollapsing();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
deleted file mode 100644
index 054543c..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ /dev/null
@@ -1,631 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.systemui.statusbar;
-
-import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_SILENT;
-
-import android.content.Context;
-import android.content.res.Resources;
-import android.os.Handler;
-import android.os.Trace;
-import android.os.UserHandle;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.R;
-import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.statusbar.dagger.CentralSurfacesModule;
-import com.android.systemui.statusbar.notification.AssistantFeedbackController;
-import com.android.systemui.statusbar.notification.DynamicChildBindController;
-import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
-import com.android.systemui.statusbar.notification.NotificationEntryManager;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.legacy.LowPriorityInflationHelper;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
-import com.android.systemui.statusbar.notification.collection.render.NotifStats;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.Assert;
-import com.android.wm.shell.bubbles.Bubbles;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Optional;
-import java.util.Stack;
-
-/**
- * NotificationViewHierarchyManager manages updating the view hierarchy of notification views based
- * on their group structure. For example, if a notification becomes bundled with another,
- * NotificationViewHierarchyManager will update the view hierarchy to reflect that. It also will
- * tell NotificationListContainer which notifications to display, and inform it of changes to those
- * notifications that might affect their display.
- */
-public class NotificationViewHierarchyManager implements DynamicPrivacyController.Listener {
-    private static final String TAG = "NotificationViewHierarchyManager";
-
-    private final Handler mHandler;
-
-    /**
-     * Re-usable map of top-level notifications to their sorted children if any.
-     * If the top-level notification doesn't have children, its key will still exist in this map
-     * with its value explicitly set to null.
-     */
-    private final HashMap<NotificationEntry, List<NotificationEntry>> mTmpChildOrderMap =
-            new HashMap<>();
-
-    // Dependencies:
-    private final DynamicChildBindController mDynamicChildBindController;
-    private final FeatureFlags mFeatureFlags;
-    protected final NotificationLockscreenUserManager mLockscreenUserManager;
-    protected final NotificationGroupManagerLegacy mGroupManager;
-    protected final VisualStabilityManager mVisualStabilityManager;
-    private final SysuiStatusBarStateController mStatusBarStateController;
-    private final NotificationEntryManager mEntryManager;
-    private final LowPriorityInflationHelper mLowPriorityInflationHelper;
-
-    /**
-     * {@code true} if notifications not part of a group should by default be rendered in their
-     * expanded state. If {@code false}, then only the first notification will be expanded if
-     * possible.
-     */
-    private final boolean mAlwaysExpandNonGroupedNotification;
-    private final Optional<Bubbles> mBubblesOptional;
-    private final DynamicPrivacyController mDynamicPrivacyController;
-    private final KeyguardBypassController mBypassController;
-    private final NotifPipelineFlags mNotifPipelineFlags;
-    private AssistantFeedbackController mAssistantFeedbackController;
-    private final KeyguardStateController mKeyguardStateController;
-    private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    private final Context mContext;
-
-    private NotificationPresenter mPresenter;
-    private NotifStackController mStackController;
-    private NotificationListContainer mListContainer;
-
-    // Used to help track down re-entrant calls to our update methods, which will cause bugs.
-    private boolean mPerformingUpdate;
-    // Hack to get around re-entrant call in onDynamicPrivacyChanged() until we can track down
-    // the problem.
-    private boolean mIsHandleDynamicPrivacyChangeScheduled;
-
-    /**
-     * Injected constructor. See {@link CentralSurfacesModule}.
-     */
-    public NotificationViewHierarchyManager(
-            Context context,
-            @Main Handler mainHandler,
-            FeatureFlags featureFlags,
-            NotificationLockscreenUserManager notificationLockscreenUserManager,
-            NotificationGroupManagerLegacy groupManager,
-            VisualStabilityManager visualStabilityManager,
-            StatusBarStateController statusBarStateController,
-            NotificationEntryManager notificationEntryManager,
-            KeyguardBypassController bypassController,
-            Optional<Bubbles> bubblesOptional,
-            DynamicPrivacyController privacyController,
-            DynamicChildBindController dynamicChildBindController,
-            LowPriorityInflationHelper lowPriorityInflationHelper,
-            AssistantFeedbackController assistantFeedbackController,
-            NotifPipelineFlags notifPipelineFlags,
-            KeyguardUpdateMonitor keyguardUpdateMonitor,
-            KeyguardStateController keyguardStateController) {
-        mContext = context;
-        mHandler = mainHandler;
-        mFeatureFlags = featureFlags;
-        mLockscreenUserManager = notificationLockscreenUserManager;
-        mBypassController = bypassController;
-        mGroupManager = groupManager;
-        mVisualStabilityManager = visualStabilityManager;
-        mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
-        mEntryManager = notificationEntryManager;
-        mNotifPipelineFlags = notifPipelineFlags;
-        Resources res = context.getResources();
-        mAlwaysExpandNonGroupedNotification =
-                res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
-        mBubblesOptional = bubblesOptional;
-        mDynamicPrivacyController = privacyController;
-        mDynamicChildBindController = dynamicChildBindController;
-        mLowPriorityInflationHelper = lowPriorityInflationHelper;
-        mAssistantFeedbackController = assistantFeedbackController;
-        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mKeyguardStateController = keyguardStateController;
-    }
-
-    public void setUpWithPresenter(NotificationPresenter presenter,
-            NotifStackController stackController,
-            NotificationListContainer listContainer) {
-        mPresenter = presenter;
-        mStackController = stackController;
-        mListContainer = listContainer;
-        if (!mNotifPipelineFlags.isNewPipelineEnabled()) {
-            mDynamicPrivacyController.addListener(this);
-        }
-    }
-
-    /**
-     * Updates the visual representation of the notifications.
-     */
-    //TODO: Rewrite this to focus on Entries, or some other data object instead of views
-    public void updateNotificationViews() {
-        Assert.isMainThread();
-        if (!mNotifPipelineFlags.checkLegacyPipelineEnabled()) {
-            return;
-        }
-        Trace.beginSection("NotificationViewHierarchyManager.updateNotificationViews");
-
-        beginUpdate();
-
-        boolean dynamicallyUnlocked = mDynamicPrivacyController.isDynamicallyUnlocked()
-                && !(mStatusBarStateController.getState() == StatusBarState.KEYGUARD
-                && mKeyguardUpdateMonitor.getUserUnlockedWithBiometricAndIsBypassing(
-                KeyguardUpdateMonitor.getCurrentUser()))
-                && !mKeyguardStateController.isKeyguardGoingAway();
-        List<NotificationEntry> activeNotifications = mEntryManager.getVisibleNotifications();
-        ArrayList<ExpandableNotificationRow> toShow = new ArrayList<>(activeNotifications.size());
-        final int N = activeNotifications.size();
-        for (int i = 0; i < N; i++) {
-            NotificationEntry ent = activeNotifications.get(i);
-            if (shouldSuppressActiveNotification(ent)) {
-                continue;
-            }
-
-            int userId = ent.getSbn().getUserId();
-
-            // Display public version of the notification if we need to redact.
-            // TODO: This area uses a lot of calls into NotificationLockscreenUserManager.
-            // We can probably move some of this code there.
-            int currentUserId = mLockscreenUserManager.getCurrentUserId();
-            boolean devicePublic = mLockscreenUserManager.isLockscreenPublicMode(currentUserId);
-            boolean userPublic = devicePublic
-                    || mLockscreenUserManager.isLockscreenPublicMode(userId);
-            if (userPublic && dynamicallyUnlocked
-                    && (userId == currentUserId || userId == UserHandle.USER_ALL
-                    || !mLockscreenUserManager.needsSeparateWorkChallenge(userId))) {
-                userPublic = false;
-            }
-            boolean needsRedaction = mLockscreenUserManager.needsRedaction(ent);
-            boolean sensitive = userPublic && needsRedaction;
-            boolean deviceSensitive = devicePublic
-                    && !mLockscreenUserManager.userAllowsPrivateNotificationsInPublic(
-                    currentUserId);
-            ent.setSensitive(sensitive, deviceSensitive);
-            ent.getRow().setNeedsRedaction(needsRedaction);
-            mLowPriorityInflationHelper.recheckLowPriorityViewAndInflate(ent, ent.getRow());
-            boolean isChildInGroup = mGroupManager.isChildInGroup(ent);
-
-            boolean groupChangesAllowed =
-                    mVisualStabilityManager.areGroupChangesAllowed() // user isn't looking at notifs
-                    || !ent.hasFinishedInitialization(); // notif recently added
-
-            NotificationEntry parent = mGroupManager.getGroupSummary(ent);
-            if (!groupChangesAllowed) {
-                // We don't to change groups while the user is looking at them
-                boolean wasChildInGroup = ent.isChildInGroup();
-                if (isChildInGroup && !wasChildInGroup) {
-                    isChildInGroup = wasChildInGroup;
-                    mVisualStabilityManager.addGroupChangesAllowedCallback(mEntryManager,
-                            false /* persistent */);
-                } else if (!isChildInGroup && wasChildInGroup) {
-                    // We allow grouping changes if the group was collapsed
-                    if (mGroupManager.isLogicalGroupExpanded(ent.getSbn())) {
-                        isChildInGroup = wasChildInGroup;
-                        parent = ent.getRow().getNotificationParent().getEntry();
-                        mVisualStabilityManager.addGroupChangesAllowedCallback(mEntryManager,
-                                false /* persistent */);
-                    }
-                }
-            }
-
-            if (isChildInGroup) {
-                List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent);
-                if (orderedChildren == null) {
-                    orderedChildren = new ArrayList<>();
-                    mTmpChildOrderMap.put(parent, orderedChildren);
-                }
-                orderedChildren.add(ent);
-            } else {
-                // Top-level notif (either a summary or single notification)
-
-                // A child may have already added its summary to mTmpChildOrderMap with a
-                // list of children. This can happen since there's no guarantee summaries are
-                // sorted before its children.
-                if (!mTmpChildOrderMap.containsKey(ent)) {
-                    // mTmpChildOrderMap's keyset is used to iterate through all entries, so it's
-                    // necessary to add each top-level notif as a key
-                    mTmpChildOrderMap.put(ent, null);
-                }
-                toShow.add(ent.getRow());
-            }
-
-        }
-
-        ArrayList<ExpandableNotificationRow> viewsToRemove = new ArrayList<>();
-        for (int i=0; i< mListContainer.getContainerChildCount(); i++) {
-            View child = mListContainer.getContainerChildAt(i);
-            if (!toShow.contains(child) && child instanceof ExpandableNotificationRow) {
-                ExpandableNotificationRow row = (ExpandableNotificationRow) child;
-
-                // Blocking helper is effectively a detached view. Don't bother removing it from the
-                // layout.
-                if (!row.isBlockingHelperShowing()) {
-                    viewsToRemove.add((ExpandableNotificationRow) child);
-                }
-            }
-        }
-
-        for (ExpandableNotificationRow viewToRemove : viewsToRemove) {
-            NotificationEntry entry = viewToRemove.getEntry();
-            if (mEntryManager.getPendingOrActiveNotif(entry.getKey()) != null
-                && !shouldSuppressActiveNotification(entry)) {
-                // we are only transferring this notification to its parent, don't generate an
-                // animation. If the notification is suppressed, this isn't a transfer.
-                mListContainer.setChildTransferInProgress(true);
-            }
-            if (viewToRemove.isSummaryWithChildren()) {
-                viewToRemove.removeAllChildren();
-            }
-            mListContainer.removeContainerView(viewToRemove);
-            mListContainer.setChildTransferInProgress(false);
-        }
-
-        removeNotificationChildren();
-
-        for (int i = 0; i < toShow.size(); i++) {
-            View v = toShow.get(i);
-            if (v.getParent() == null) {
-                mVisualStabilityManager.notifyViewAddition(v);
-                mListContainer.addContainerView(v);
-            } else if (!mListContainer.containsView(v)) {
-                // the view is added somewhere else. Let's make sure
-                // the ordering works properly below, by excluding these
-                toShow.remove(v);
-                i--;
-            }
-        }
-
-        addNotificationChildrenAndSort();
-
-        // So after all this work notifications still aren't sorted correctly.
-        // Let's do that now by advancing through toShow and mListContainer in
-        // lock-step, making sure mListContainer matches what we see in toShow.
-        int j = 0;
-        for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
-            View child = mListContainer.getContainerChildAt(i);
-            if (!(child instanceof ExpandableNotificationRow)) {
-                // We don't care about non-notification views.
-                continue;
-            }
-            if (((ExpandableNotificationRow) child).isBlockingHelperShowing()) {
-                // Don't count/reorder notifications that are showing the blocking helper!
-                continue;
-            }
-
-            ExpandableNotificationRow targetChild = toShow.get(j);
-            if (child != targetChild) {
-                // Oops, wrong notification at this position. Put the right one
-                // here and advance both lists.
-                if (mVisualStabilityManager.canReorderNotification(targetChild)) {
-                    mListContainer.changeViewPosition(targetChild, i);
-                } else {
-                    mVisualStabilityManager.addReorderingAllowedCallback(mEntryManager,
-                            false  /* persistent */);
-                }
-            }
-            j++;
-
-        }
-
-        mDynamicChildBindController.updateContentViews(mTmpChildOrderMap);
-        mVisualStabilityManager.onReorderingFinished();
-        // clear the map again for the next usage
-        mTmpChildOrderMap.clear();
-
-        updateRowStatesInternal();
-        updateNotifStats();
-
-        mListContainer.onNotificationViewUpdateFinished();
-
-        endUpdate();
-        Trace.endSection();
-    }
-
-    /**
-     * In the spirit of unidirectional data flow, calculate this information when the notification
-     * views are updated, and set it once, speeding up lookups later.
-     * This is analogous to logic in the
-     * {@link com.android.systemui.statusbar.notification.collection.coordinator.StackCoordinator}
-     */
-    private void updateNotifStats() {
-        Trace.beginSection("NotificationViewHierarchyManager.updateNotifStats");
-        boolean hasNonClearableAlertingNotifs = false;
-        boolean hasClearableAlertingNotifs = false;
-        boolean hasNonClearableSilentNotifs = false;
-        boolean hasClearableSilentNotifs = false;
-        final int childCount = mListContainer.getContainerChildCount();
-        int visibleTopLevelEntries = 0;
-        for (int i = 0; i < childCount; i++) {
-            View child = mListContainer.getContainerChildAt(i);
-            if (child == null || child.getVisibility() == View.GONE) {
-                continue;
-            }
-            if (!(child instanceof ExpandableNotificationRow)) {
-                continue;
-            }
-            final ExpandableNotificationRow row = (ExpandableNotificationRow) child;
-            boolean isSilent = row.getEntry().getBucket() == BUCKET_SILENT;
-            // NOTE: NotificationEntry.isClearable() will internally check group children to ensure
-            //  the group itself definitively clearable.
-            boolean isClearable = row.getEntry().isClearable();
-            visibleTopLevelEntries++;
-            if (isSilent) {
-                if (isClearable) {
-                    hasClearableSilentNotifs = true;
-                } else {  // !isClearable
-                    hasNonClearableSilentNotifs = true;
-                }
-            } else {  // !isSilent
-                if (isClearable) {
-                    hasClearableAlertingNotifs = true;
-                } else {  // !isClearable
-                    hasNonClearableAlertingNotifs = true;
-                }
-            }
-        }
-        mStackController.setNotifStats(new NotifStats(
-                visibleTopLevelEntries /* numActiveNotifs */,
-                hasNonClearableAlertingNotifs /* hasNonClearableAlertingNotifs */,
-                hasClearableAlertingNotifs /* hasClearableAlertingNotifs */,
-                hasNonClearableSilentNotifs /* hasNonClearableSilentNotifs */,
-                hasClearableSilentNotifs /* hasClearableSilentNotifs */
-        ));
-        Trace.endSection();
-    }
-
-    /**
-     * Should a notification entry from the active list be suppressed and not show?
-     */
-    private boolean shouldSuppressActiveNotification(NotificationEntry ent) {
-        final boolean isBubbleNotificationSuppressedFromShade = mBubblesOptional.isPresent()
-                && mBubblesOptional.get().isBubbleNotificationSuppressedFromShade(
-                        ent.getKey(), ent.getSbn().getGroupKey());
-        if (ent.isRowDismissed() || ent.isRowRemoved()
-                || isBubbleNotificationSuppressedFromShade) {
-            // we want to suppress removed notifications because they could
-            // temporarily become children if they were isolated before.
-            return true;
-        }
-        return false;
-    }
-
-    private void addNotificationChildrenAndSort() {
-        // Let's now add all notification children which are missing
-        boolean orderChanged = false;
-        ArrayList<ExpandableNotificationRow> orderedRows = new ArrayList<>();
-        for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
-            View view = mListContainer.getContainerChildAt(i);
-            if (!(view instanceof ExpandableNotificationRow)) {
-                // We don't care about non-notification views.
-                continue;
-            }
-
-            ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
-            List<ExpandableNotificationRow> children = parent.getAttachedChildren();
-            List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent.getEntry());
-            if (orderedChildren == null) {
-                // Not a group
-                continue;
-            }
-            parent.setUntruncatedChildCount(orderedChildren.size());
-            for (int childIndex = 0; childIndex < orderedChildren.size(); childIndex++) {
-                ExpandableNotificationRow childView = orderedChildren.get(childIndex).getRow();
-                if (children == null || !children.contains(childView)) {
-                    if (childView.getParent() != null) {
-                        Log.wtf(TAG, "trying to add a notification child that already has "
-                                + "a parent. class:" + childView.getParent().getClass()
-                                + "\n child: " + childView);
-                        // This shouldn't happen. We can recover by removing it though.
-                        ((ViewGroup) childView.getParent()).removeView(childView);
-                    }
-                    mVisualStabilityManager.notifyViewAddition(childView);
-                    parent.addChildNotification(childView, childIndex);
-                    mListContainer.notifyGroupChildAdded(childView);
-                }
-                orderedRows.add(childView);
-            }
-
-            // Finally after removing and adding has been performed we can apply the order.
-            orderChanged |= parent.applyChildOrder(orderedRows, mVisualStabilityManager,
-                    mEntryManager);
-            orderedRows.clear();
-        }
-        if (orderChanged) {
-            mListContainer.generateChildOrderChangedEvent();
-        }
-    }
-
-    private void removeNotificationChildren() {
-        // First let's remove all children which don't belong in the parents
-        ArrayList<ExpandableNotificationRow> toRemove = new ArrayList<>();
-        for (int i = 0; i < mListContainer.getContainerChildCount(); i++) {
-            View view = mListContainer.getContainerChildAt(i);
-            if (!(view instanceof ExpandableNotificationRow)) {
-                // We don't care about non-notification views.
-                continue;
-            }
-
-            ExpandableNotificationRow parent = (ExpandableNotificationRow) view;
-            List<ExpandableNotificationRow> children = parent.getAttachedChildren();
-            List<NotificationEntry> orderedChildren = mTmpChildOrderMap.get(parent.getEntry());
-
-            if (children != null) {
-                toRemove.clear();
-                for (ExpandableNotificationRow childRow : children) {
-                    if ((orderedChildren == null
-                            || !orderedChildren.contains(childRow.getEntry()))
-                            && !childRow.keepInParent()) {
-                        toRemove.add(childRow);
-                    }
-                }
-                for (ExpandableNotificationRow remove : toRemove) {
-                    parent.removeChildNotification(remove);
-                    if (mEntryManager.getActiveNotificationUnfiltered(
-                            remove.getEntry().getSbn().getKey()) == null) {
-                        // We only want to add an animation if the view is completely removed
-                        // otherwise it's just a transfer
-                        mListContainer.notifyGroupChildRemoved(remove,
-                                parent.getChildrenContainer());
-                    }
-                }
-            }
-        }
-    }
-
-    /**
-     * Updates expanded, dimmed and locked states of notification rows.
-     */
-    public void updateRowStates() {
-        Assert.isMainThread();
-        if (!mNotifPipelineFlags.checkLegacyPipelineEnabled()) {
-            return;
-        }
-
-        beginUpdate();
-        updateRowStatesInternal();
-        endUpdate();
-    }
-
-    private void updateRowStatesInternal() {
-        Trace.beginSection("NotificationViewHierarchyManager.updateRowStates");
-        final int N = mListContainer.getContainerChildCount();
-
-        int visibleNotifications = 0;
-        boolean onKeyguard =
-                mStatusBarStateController.getCurrentOrUpcomingState() == StatusBarState.KEYGUARD;
-        Stack<ExpandableNotificationRow> stack = new Stack<>();
-        for (int i = N - 1; i >= 0; i--) {
-            View child = mListContainer.getContainerChildAt(i);
-            if (!(child instanceof ExpandableNotificationRow)) {
-                continue;
-            }
-            stack.push((ExpandableNotificationRow) child);
-        }
-        while(!stack.isEmpty()) {
-            ExpandableNotificationRow row = stack.pop();
-            NotificationEntry entry = row.getEntry();
-            boolean isChildNotification = mGroupManager.isChildInGroup(entry);
-
-            if (!onKeyguard) {
-                // If mAlwaysExpandNonGroupedNotification is false, then only expand the
-                // very first notification and if it's not a child of grouped notifications.
-                row.setSystemExpanded(mAlwaysExpandNonGroupedNotification
-                        || (visibleNotifications == 0 && !isChildNotification
-                        && !row.isLowPriority()));
-            }
-
-            int userId = entry.getSbn().getUserId();
-            boolean suppressedSummary = mGroupManager.isSummaryOfSuppressedGroup(
-                    entry.getSbn()) && !entry.isRowRemoved();
-            boolean showOnKeyguard = mLockscreenUserManager.shouldShowOnKeyguard(entry);
-            if (!showOnKeyguard) {
-                // min priority notifications should show if their summary is showing
-                if (mGroupManager.isChildInGroup(entry)) {
-                    NotificationEntry summary = mGroupManager.getLogicalGroupSummary(entry);
-                    if (summary != null && mLockscreenUserManager.shouldShowOnKeyguard(summary)) {
-                        showOnKeyguard = true;
-                    }
-                }
-            }
-            if (suppressedSummary
-                    || mLockscreenUserManager.shouldHideNotifications(userId)
-                    || (onKeyguard && !showOnKeyguard)) {
-                entry.getRow().setVisibility(View.GONE);
-            } else {
-                boolean wasGone = entry.getRow().getVisibility() == View.GONE;
-                if (wasGone) {
-                    entry.getRow().setVisibility(View.VISIBLE);
-                }
-                if (!isChildNotification && !entry.getRow().isRemoved()) {
-                    if (wasGone) {
-                        // notify the scroller of a child addition
-                        mListContainer.generateAddAnimation(entry.getRow(),
-                                !showOnKeyguard /* fromMoreCard */);
-                    }
-                    visibleNotifications++;
-                }
-            }
-            if (row.isSummaryWithChildren()) {
-                List<ExpandableNotificationRow> notificationChildren =
-                        row.getAttachedChildren();
-                int size = notificationChildren.size();
-                for (int i = size - 1; i >= 0; i--) {
-                    stack.push(notificationChildren.get(i));
-                }
-            }
-            row.setFeedbackIcon(mAssistantFeedbackController.getFeedbackIcon(entry));
-            row.setLastAudiblyAlertedMs(entry.getLastAudiblyAlertedMs());
-        }
-
-        Trace.beginSection("NotificationPresenter#onUpdateRowStates");
-        mPresenter.onUpdateRowStates();
-        Trace.endSection();
-        Trace.endSection();
-    }
-
-    @Override
-    public void onDynamicPrivacyChanged() {
-        mNotifPipelineFlags.assertLegacyPipelineEnabled();
-        if (mPerformingUpdate) {
-            Log.w(TAG, "onDynamicPrivacyChanged made a re-entrant call");
-        }
-        // This listener can be called from updateNotificationViews() via a convoluted listener
-        // chain, so we post here to prevent a re-entrant call. See b/136186188
-        // TODO: Refactor away the need for this
-        if (!mIsHandleDynamicPrivacyChangeScheduled) {
-            mIsHandleDynamicPrivacyChangeScheduled = true;
-            mHandler.post(this::onHandleDynamicPrivacyChanged);
-        }
-    }
-
-    private void onHandleDynamicPrivacyChanged() {
-        mIsHandleDynamicPrivacyChangeScheduled = false;
-        updateNotificationViews();
-    }
-
-    private void beginUpdate() {
-        if (mPerformingUpdate) {
-            Log.wtf(TAG, "Re-entrant code during update", new Exception());
-        }
-        mPerformingUpdate = true;
-    }
-
-    private void endUpdate() {
-        if (!mPerformingUpdate) {
-            Log.wtf(TAG, "Manager state has become desynced", new Exception());
-        }
-        mPerformingUpdate = false;
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
index 9e77dbc..48e3450 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesDependenciesModule.java
@@ -23,13 +23,11 @@
 
 import com.android.internal.jank.InteractionJankMonitor;
 import com.android.internal.statusbar.IStatusBarService;
-import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -42,23 +40,16 @@
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.RemoteInputNotificationRebuilder;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.commandline.CommandRegistry;
 import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler;
-import com.android.systemui.statusbar.notification.AssistantFeedbackController;
-import com.android.systemui.statusbar.notification.DynamicChildBindController;
-import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotifCollection;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
-import com.android.systemui.statusbar.notification.collection.legacy.LowPriorityInflationHelper;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
 import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
@@ -73,13 +64,11 @@
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallFlags;
 import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLogger;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.statusbar.policy.RemoteInputUriController;
 import com.android.systemui.statusbar.window.StatusBarWindowController;
 import com.android.systemui.tracing.ProtoTracer;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.time.SystemClock;
-import com.android.wm.shell.bubbles.Bubbles;
 
 import java.util.Optional;
 import java.util.concurrent.Executor;
@@ -182,47 +171,6 @@
     NotificationRemoteInputManager.Callback provideNotificationRemoteInputManagerCallback(
             StatusBarRemoteInputCallback callbackImpl);
 
-    /** */
-    @SysUISingleton
-    @Provides
-    static NotificationViewHierarchyManager provideNotificationViewHierarchyManager(
-            Context context,
-            @Main Handler mainHandler,
-            FeatureFlags featureFlags,
-            NotificationLockscreenUserManager notificationLockscreenUserManager,
-            NotificationGroupManagerLegacy groupManager,
-            VisualStabilityManager visualStabilityManager,
-            StatusBarStateController statusBarStateController,
-            NotificationEntryManager notificationEntryManager,
-            KeyguardBypassController bypassController,
-            Optional<Bubbles> bubblesOptional,
-            DynamicPrivacyController privacyController,
-            DynamicChildBindController dynamicChildBindController,
-            LowPriorityInflationHelper lowPriorityInflationHelper,
-            AssistantFeedbackController assistantFeedbackController,
-            NotifPipelineFlags notifPipelineFlags,
-            KeyguardUpdateMonitor keyguardUpdateMonitor,
-            KeyguardStateController keyguardStateController) {
-        return new NotificationViewHierarchyManager(
-                context,
-                mainHandler,
-                featureFlags,
-                notificationLockscreenUserManager,
-                groupManager,
-                visualStabilityManager,
-                statusBarStateController,
-                notificationEntryManager,
-                bypassController,
-                bubblesOptional,
-                privacyController,
-                dynamicChildBindController,
-                lowPriorityInflationHelper,
-                assistantFeedbackController,
-                notifPipelineFlags,
-                keyguardUpdateMonitor,
-                keyguardStateController);
-    }
-
     /**
      * Provides our instance of CommandQueue which is considered optional.
      */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
index 0c6a91f..7fbdd35 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotifPipelineFlags.kt
@@ -29,10 +29,6 @@
     val featureFlags: FeatureFlags
 ) {
     fun checkLegacyPipelineEnabled(): Boolean {
-        if (!isNewPipelineEnabled()) {
-            return true
-        }
-
         if (Compile.IS_DEBUG) {
             Toast.makeText(context, "Old pipeline code running!", Toast.LENGTH_SHORT).show()
         }
@@ -45,11 +41,6 @@
         return false
     }
 
-    fun assertLegacyPipelineEnabled(): Unit =
-        check(!isNewPipelineEnabled()) { "Old pipeline code running w/ new pipeline enabled" }
-
-    fun isNewPipelineEnabled(): Boolean = true
-
     fun isDevLoggingEnabled(): Boolean =
         featureFlags.isEnabled(Flags.NOTIFICATION_PIPELINE_DEVELOPER_LOGGING)
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 852d5f7..7583a98 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -46,11 +46,9 @@
 import com.android.systemui.statusbar.NotificationLifetimeExtender;
 import com.android.systemui.statusbar.NotificationListener;
 import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
-import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
 import com.android.systemui.statusbar.NotificationUiAdjustment;
-import com.android.systemui.statusbar.notification.collection.NotifLiveDataStoreImpl;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder;
 import com.android.systemui.statusbar.notification.collection.legacy.LegacyNotificationRanker;
@@ -113,7 +111,6 @@
     private final Lazy<NotificationRemoteInputManager> mRemoteInputManagerLazy;
     private final LeakDetector mLeakDetector;
     private final IStatusBarService mStatusBarService;
-    private final NotifLiveDataStoreImpl mNotifLiveDataStore;
     private final DumpManager mDumpManager;
     private final Executor mBgExecutor;
 
@@ -141,7 +138,6 @@
     private final List<NotifCollectionListener> mNotifCollectionListeners = new ArrayList<>();
 
     private LegacyNotificationRanker mRanker = new LegacyNotificationRankerStub();
-    private NotificationPresenter mPresenter;
     private RankingMap mLatestRankingMap;
 
     @VisibleForTesting
@@ -161,7 +157,6 @@
             Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy,
             LeakDetector leakDetector,
             IStatusBarService statusBarService,
-            NotifLiveDataStoreImpl notifLiveDataStore,
             DumpManager dumpManager,
             @Background Executor bgExecutor
     ) {
@@ -172,7 +167,6 @@
         mRemoteInputManagerLazy = notificationRemoteInputManagerLazy;
         mLeakDetector = leakDetector;
         mStatusBarService = statusBarService;
-        mNotifLiveDataStore = notifLiveDataStore;
         mDumpManager = dumpManager;
         mBgExecutor = bgExecutor;
     }
@@ -250,10 +244,6 @@
         mRemoveInterceptors.remove(interceptor);
     }
 
-    public void setUpWithPresenter(NotificationPresenter presenter) {
-        mPresenter = presenter;
-    }
-
     /** Adds multiple {@link NotificationLifetimeExtender}s. */
     public void addNotificationLifetimeExtenders(List<NotificationLifetimeExtender> extenders) {
         for (NotificationLifetimeExtender extender : extenders) {
@@ -663,11 +653,6 @@
             listener.onEntryBind(entry, notification);
         }
 
-        // Construct the expanded view.
-        if (!mNotifPipelineFlags.isNewPipelineEnabled()) {
-            mNotificationRowBinderLazy.get().inflateViews(entry, null, mInflationCallback);
-        }
-
         mPendingNotifications.put(key, entry);
         mLogger.logNotifAdded(entry.getKey());
         for (NotificationEntryListener listener : mNotificationEntryListeners) {
@@ -724,10 +709,6 @@
             listener.onEntryUpdated(entry, fromSystem);
         }
 
-        if (!mNotifPipelineFlags.isNewPipelineEnabled()) {
-            mNotificationRowBinderLazy.get().inflateViews(entry, null, mInflationCallback);
-        }
-
         updateNotifications("updateNotificationInternal");
 
         for (NotificationEntryListener listener : mNotificationEntryListeners) {
@@ -752,17 +733,7 @@
      * @param reason why the notifications are updating
      */
     public void updateNotifications(String reason) {
-        if (mNotifPipelineFlags.isNewPipelineEnabled()) {
-            mLogger.logUseWhileNewPipelineActive("updateNotifications", reason);
-            return;
-        }
-        Trace.beginSection("NotificationEntryManager.updateNotifications");
-        reapplyFilterAndSort(reason);
-        if (mPresenter != null) {
-            mPresenter.updateNotificationViews(reason);
-        }
-        mNotifLiveDataStore.setActiveNotifList(getVisibleNotifications());
-        Trace.endSection();
+        mLogger.logUseWhileNewPipelineActive("updateNotifications", reason);
     }
 
     public void updateNotificationRanking(RankingMap rankingMap) {
@@ -939,26 +910,12 @@
 
     /** Resorts / filters the current notification set with the current RankingMap */
     public void reapplyFilterAndSort(String reason) {
-        if (mNotifPipelineFlags.isNewPipelineEnabled()) {
-            mLogger.logUseWhileNewPipelineActive("reapplyFilterAndSort", reason);
-            return;
-        }
-        Trace.beginSection("NotificationEntryManager.reapplyFilterAndSort");
-        updateRankingAndSort(mRanker.getRankingMap(), reason);
-        Trace.endSection();
+        mLogger.logUseWhileNewPipelineActive("reapplyFilterAndSort", reason);
     }
 
     /** Calls to NotificationRankingManager and updates mSortedAndFiltered */
     private void updateRankingAndSort(RankingMap rankingMap, String reason) {
-        if (mNotifPipelineFlags.isNewPipelineEnabled()) {
-            mLogger.logUseWhileNewPipelineActive("updateRankingAndSort", reason);
-            return;
-        }
-        Trace.beginSection("NotificationEntryManager.updateRankingAndSort");
-        mSortedAndFiltered.clear();
-        mSortedAndFiltered.addAll(mRanker.updateRanking(
-                rankingMap, mActiveNotifications.values(), reason));
-        Trace.endSection();
+        mLogger.logUseWhileNewPipelineActive("updateRankingAndSort", reason);
     }
 
     /** dump the current active notification list. Called from CentralSurfaces */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
index 945bc72..9e5dab1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/ListAttachState.kt
@@ -68,9 +68,6 @@
      */
     var stableIndex: Int = -1
 
-    /** Access the index of the [section] or -1 if the entry does not have one */
-    val sectionIndex: Int get() = section?.index ?: -1
-
     /** Copies the state of another instance. */
     fun clone(other: ListAttachState) {
         parent = other.parent
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 5198d82..075a0dc 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
@@ -1039,25 +1039,22 @@
         NotifSection currentSection = requireNonNull(notifList.get(0).getSection());
         int sectionMemberIndex = 0;
         for (int i = 0; i < notifList.size(); i++) {
-            final ListEntry entry = notifList.get(i);
+            ListEntry entry = notifList.get(i);
             NotifSection section = requireNonNull(entry.getSection());
             if (section.getIndex() != currentSection.getIndex()) {
                 sectionMemberIndex = 0;
                 currentSection = section;
             }
-            entry.getAttachState().setStableIndex(sectionMemberIndex++);
+            entry.getAttachState().setStableIndex(sectionMemberIndex);
             if (entry instanceof GroupEntry) {
-                final GroupEntry parent = (GroupEntry) entry;
-                final NotificationEntry summary = parent.getSummary();
-                if (summary != null) {
-                    summary.getAttachState().setStableIndex(sectionMemberIndex++);
-                }
-                final List<NotificationEntry> children = parent.getChildren();
-                for (int j = 0; j < children.size(); j++) {
-                    final NotificationEntry child = children.get(j);
-                    child.getAttachState().setStableIndex(sectionMemberIndex++);
+                GroupEntry parent = (GroupEntry) entry;
+                for (int j = 0; j < parent.getChildren().size(); j++) {
+                    entry = parent.getChildren().get(j);
+                    entry.getAttachState().setStableIndex(sectionMemberIndex);
+                    sectionMemberIndex++;
                 }
             }
+            sectionMemberIndex++;
         }
     }
 
@@ -1197,9 +1194,9 @@
                 o2.getSectionIndex());
         if (cmp != 0) return cmp;
 
-        cmp = Integer.compare(
-                getStableOrderIndex(o1),
-                getStableOrderIndex(o2));
+        int index1 = canReorder(o1) ? -1 : o1.getPreviousAttachState().getStableIndex();
+        int index2 = canReorder(o2) ? -1 : o2.getPreviousAttachState().getStableIndex();
+        cmp = Integer.compare(index1, index2);
         if (cmp != 0) return cmp;
 
         NotifComparator sectionComparator = getSectionComparator(o1, o2);
@@ -1213,32 +1210,31 @@
             if (cmp != 0) return cmp;
         }
 
-        cmp = Integer.compare(
-                o1.getRepresentativeEntry().getRanking().getRank(),
-                o2.getRepresentativeEntry().getRanking().getRank());
+        final NotificationEntry rep1 = o1.getRepresentativeEntry();
+        final NotificationEntry rep2 = o2.getRepresentativeEntry();
+            cmp = rep1.getRanking().getRank() - rep2.getRanking().getRank();
         if (cmp != 0) return cmp;
 
-        cmp = -1 * Long.compare(
-                o1.getRepresentativeEntry().getSbn().getNotification().when,
-                o2.getRepresentativeEntry().getSbn().getNotification().when);
+        cmp = Long.compare(
+                rep2.getSbn().getNotification().when,
+                rep1.getSbn().getNotification().when);
         return cmp;
     };
 
 
     private final Comparator<NotificationEntry> mGroupChildrenComparator = (o1, o2) -> {
-        int cmp = Integer.compare(
-                getStableOrderIndex(o1),
-                getStableOrderIndex(o2));
+        int index1 = canReorder(o1) ? -1 : o1.getPreviousAttachState().getStableIndex();
+        int index2 = canReorder(o2) ? -1 : o2.getPreviousAttachState().getStableIndex();
+        int cmp = Integer.compare(index1, index2);
         if (cmp != 0) return cmp;
 
-        cmp = Integer.compare(
-                o1.getRepresentativeEntry().getRanking().getRank(),
-                o2.getRepresentativeEntry().getRanking().getRank());
+        cmp = o1.getRepresentativeEntry().getRanking().getRank()
+                - o2.getRepresentativeEntry().getRanking().getRank();
         if (cmp != 0) return cmp;
 
-        cmp = -1 * Long.compare(
-                o1.getRepresentativeEntry().getSbn().getNotification().when,
-                o2.getRepresentativeEntry().getSbn().getNotification().when);
+        cmp = Long.compare(
+                o2.getRepresentativeEntry().getSbn().getNotification().when,
+                o1.getRepresentativeEntry().getSbn().getNotification().when);
         return cmp;
     };
 
@@ -1248,21 +1244,8 @@
      */
     private boolean mForceReorderable = false;
 
-    private int getStableOrderIndex(ListEntry entry) {
-        if (mForceReorderable) {
-            // this is used to determine if the list is correctly sorted
-            return -1;
-        }
-        if (getStabilityManager().isEntryReorderingAllowed(entry)) {
-            // let the stability manager constrain or allow reordering
-            return -1;
-        }
-        if (entry.getAttachState().getSectionIndex()
-                != entry.getPreviousAttachState().getSectionIndex()) {
-            // stable index is only valid within the same section; otherwise we allow reordering
-            return -1;
-        }
-        return entry.getPreviousAttachState().getStableIndex();
+    private boolean canReorder(ListEntry entry) {
+        return mForceReorderable || getStabilityManager().isEntryReorderingAllowed(entry);
     }
 
     private boolean applyFilters(NotificationEntry entry, long now, List<NotifFilter> filters) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
index 4c406e3..d8dae5d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/listbuilder/ShadeListBuilderLogger.kt
@@ -336,7 +336,7 @@
     }
 
     fun logPipelineRunSuppressed() =
-        buffer.log(TAG, INFO, {}) { "Suppressing pipeline run during animation." }
+        buffer.log(TAG, INFO, {}, { "Suppressing pipeline run during animation." })
 }
 
 private const val TAG = "ShadeListBuilder"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
index fcf35bf..fac234c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java
@@ -127,7 +127,6 @@
             Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy,
             LeakDetector leakDetector,
             IStatusBarService statusBarService,
-            NotifLiveDataStoreImpl notifLiveDataStore,
             DumpManager dumpManager,
             @Background Executor bgExecutor) {
         return new NotificationEntryManager(
@@ -138,7 +137,6 @@
                 notificationRemoteInputManagerLazy,
                 leakDetector,
                 statusBarService,
-                notifLiveDataStore,
                 dumpManager,
                 bgExecutor);
     }
@@ -174,7 +172,6 @@
                 accessibilityManager,
                 highPriorityProvider,
                 notificationManager,
-                notificationEntryManager,
                 peopleSpaceWidgetManager,
                 launcherApps,
                 shortcutManager,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
index a72b381..558fd62 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java
@@ -37,7 +37,6 @@
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
-import com.android.systemui.statusbar.notification.NotificationFilter;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -60,13 +59,11 @@
     private final List<NotificationInterruptSuppressor> mSuppressors = new ArrayList<>();
     private final StatusBarStateController mStatusBarStateController;
     private final KeyguardStateController mKeyguardStateController;
-    private final NotificationFilter mNotificationFilter;
     private final ContentResolver mContentResolver;
     private final PowerManager mPowerManager;
     private final IDreamManager mDreamManager;
     private final AmbientDisplayConfiguration mAmbientDisplayConfiguration;
     private final BatteryController mBatteryController;
-    private final ContentObserver mHeadsUpObserver;
     private final HeadsUpManager mHeadsUpManager;
     private final NotificationInterruptLogger mLogger;
     private final NotifPipelineFlags mFlags;
@@ -81,7 +78,6 @@
             PowerManager powerManager,
             IDreamManager dreamManager,
             AmbientDisplayConfiguration ambientDisplayConfiguration,
-            NotificationFilter notificationFilter,
             BatteryController batteryController,
             StatusBarStateController statusBarStateController,
             KeyguardStateController keyguardStateController,
@@ -95,14 +91,13 @@
         mDreamManager = dreamManager;
         mBatteryController = batteryController;
         mAmbientDisplayConfiguration = ambientDisplayConfiguration;
-        mNotificationFilter = notificationFilter;
         mStatusBarStateController = statusBarStateController;
         mKeyguardStateController = keyguardStateController;
         mHeadsUpManager = headsUpManager;
         mLogger = logger;
         mFlags = flags;
         mKeyguardNotificationVisibilityProvider = keyguardNotificationVisibilityProvider;
-        mHeadsUpObserver = new ContentObserver(mainHandler) {
+        ContentObserver headsUpObserver = new ContentObserver(mainHandler) {
             @Override
             public void onChange(boolean selfChange) {
                 boolean wasUsing = mUseHeadsUp;
@@ -125,12 +120,12 @@
             mContentResolver.registerContentObserver(
                     Settings.Global.getUriFor(Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED),
                     true,
-                    mHeadsUpObserver);
+                    headsUpObserver);
             mContentResolver.registerContentObserver(
                     Settings.Global.getUriFor(SETTING_HEADS_UP_TICKER), true,
-                    mHeadsUpObserver);
+                    headsUpObserver);
         }
-        mHeadsUpObserver.onChange(true); // set up
+        headsUpObserver.onChange(true); // set up
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
index c228af4..c4ff259 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationGutsManager.java
@@ -21,7 +21,6 @@
 
 import android.app.INotificationManager;
 import android.app.NotificationChannel;
-import android.appwidget.AppWidgetManager;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.LauncherApps;
@@ -63,13 +62,11 @@
 import com.android.systemui.statusbar.StatusBarStateControllerImpl;
 import com.android.systemui.statusbar.notification.AssistantFeedbackController;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
-import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewListener;
 import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager;
 import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
-import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSaveListener;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.phone.CentralSurfaces;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -114,7 +111,6 @@
     private NotificationPresenter mPresenter;
     private NotificationActivityStarter mNotificationActivityStarter;
     private NotificationListContainer mListContainer;
-    private CheckSaveListener mCheckSaveListener;
     private OnSettingsClickListener mOnSettingsClickListener;
     @VisibleForTesting
     protected String mKeyToRemoveOnGutsClosed;
@@ -131,7 +127,6 @@
     private final UserContextProvider mContextTracker;
     private final UiEventLogger mUiEventLogger;
     private final ShadeController mShadeController;
-    private final AppWidgetManager mAppWidgetManager;
     private NotifGutsViewListener mGutsListener;
 
     /**
@@ -144,7 +139,6 @@
             AccessibilityManager accessibilityManager,
             HighPriorityProvider highPriorityProvider,
             INotificationManager notificationManager,
-            NotificationEntryManager notificationEntryManager,
             PeopleSpaceWidgetManager peopleSpaceWidgetManager,
             LauncherApps launcherApps,
             ShortcutManager shortcutManager,
@@ -173,17 +167,15 @@
         mUiEventLogger = uiEventLogger;
         mOnUserInteractionCallback = onUserInteractionCallback;
         mShadeController = shadeController;
-        mAppWidgetManager = AppWidgetManager.getInstance(context);
 
         dumpManager.registerDumpable(this);
     }
 
     public void setUpWithPresenter(NotificationPresenter presenter,
             NotificationListContainer listContainer,
-            CheckSaveListener checkSave, OnSettingsClickListener onSettingsClick) {
+            OnSettingsClickListener onSettingsClick) {
         mPresenter = presenter;
         mListContainer = listContainer;
-        mCheckSaveListener = checkSave;
         mOnSettingsClickListener = onSettingsClick;
     }
 
@@ -218,14 +210,11 @@
     }
 
     private void startAppDetailsSettingsActivity(String packageName, final int appUid,
-            final NotificationChannel channel, ExpandableNotificationRow row) {
+            ExpandableNotificationRow row) {
         final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
         intent.setData(Uri.fromParts("package", packageName, null));
         intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName);
         intent.putExtra(Settings.EXTRA_APP_UID, appUid);
-        if (channel != null) {
-            intent.putExtra(EXTRA_FRAGMENT_ARG_KEY, channel.getId());
-        }
         mNotificationActivityStarter.startNotificationGutsIntent(intent, appUid, row);
     }
 
@@ -233,7 +222,7 @@
             ExpandableNotificationRow row) {
         if (ops.contains(OP_SYSTEM_ALERT_WINDOW)) {
             if (ops.contains(OP_CAMERA) || ops.contains(OP_RECORD_AUDIO)) {
-                startAppDetailsSettingsActivity(pkg, uid, null, row);
+                startAppDetailsSettingsActivity(pkg, uid, row);
             } else {
                 Intent intent = new Intent(Settings.ACTION_MANAGE_APP_OVERLAY_PERMISSION);
                 intent.setData(Uri.fromParts("package", pkg, null));
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 1d3b7fe..333f6b9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -196,14 +196,12 @@
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
 import com.android.systemui.statusbar.NotificationShelfController;
-import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.PowerButtonReveal;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.core.StatusBarInitializer;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationActivityStarter;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationLaunchAnimatorControllerProvider;
@@ -515,7 +513,6 @@
     protected final NotificationEntryManager mEntryManager;
     private final NotificationGutsManager mGutsManager;
     private final NotificationLogger mNotificationLogger;
-    private final NotificationViewHierarchyManager mViewHierarchyManager;
     private final PanelExpansionStateManager mPanelExpansionStateManager;
     private final KeyguardViewMediator mKeyguardViewMediator;
     protected final NotificationInterruptStateProvider mNotificationInterruptStateProvider;
@@ -651,7 +648,6 @@
     private final Optional<Bubbles> mBubblesOptional;
     private final Bubbles.BubbleExpandListener mBubbleExpandListener;
     private final Optional<StartingSurface> mStartingSurfaceOptional;
-    private final NotifPipelineFlags mNotifPipelineFlags;
 
     private final ActivityIntentHelper mActivityIntentHelper;
     private NotificationStackScrollLayoutController mStackScrollerController;
@@ -693,7 +689,6 @@
             NotificationGutsManager notificationGutsManager,
             NotificationLogger notificationLogger,
             NotificationInterruptStateProvider notificationInterruptStateProvider,
-            NotificationViewHierarchyManager notificationViewHierarchyManager,
             PanelExpansionStateManager panelExpansionStateManager,
             KeyguardViewMediator keyguardViewMediator,
             DisplayMetrics displayMetrics,
@@ -756,7 +751,6 @@
             WallpaperManager wallpaperManager,
             Optional<StartingSurface> startingSurfaceOptional,
             ActivityLaunchAnimator activityLaunchAnimator,
-            NotifPipelineFlags notifPipelineFlags,
             InteractionJankMonitor jankMonitor,
             DeviceStateManager deviceStateManager,
             WiredChargingRippleController wiredChargingRippleController,
@@ -783,7 +777,6 @@
         mGutsManager = notificationGutsManager;
         mNotificationLogger = notificationLogger;
         mNotificationInterruptStateProvider = notificationInterruptStateProvider;
-        mViewHierarchyManager = notificationViewHierarchyManager;
         mPanelExpansionStateManager = panelExpansionStateManager;
         mKeyguardViewMediator = keyguardViewMediator;
         mDisplayMetrics = displayMetrics;
@@ -844,7 +837,6 @@
 
         mLockscreenShadeTransitionController = lockscreenShadeTransitionController;
         mStartingSurfaceOptional = startingSurfaceOptional;
-        mNotifPipelineFlags = notifPipelineFlags;
         mDreamManager = dreamManager;
         lockscreenShadeTransitionController.setCentralSurfaces(this);
         statusBarWindowStateController.addListener(this::onStatusBarWindowStateChanged);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index 43a5451..dc77d10 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone;
 
+import static com.android.internal.util.Preconditions.checkNotNull;
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE;
 import static com.android.systemui.doze.util.BurnInHelperKt.getBurnInOffset;
 import static com.android.systemui.wallet.controller.QuickAccessWalletController.WalletChangeEvent.DEFAULT_PAYMENT_APP_CHANGE;
@@ -57,6 +58,8 @@
 import com.android.systemui.controls.management.ControlsListingController;
 import com.android.systemui.controls.ui.ControlsActivity;
 import com.android.systemui.controls.ui.ControlsUiController;
+import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaViewBinder;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.plugins.ActivityStarter;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qrcodescanner.controller.QRCodeScannerController;
@@ -125,6 +128,9 @@
         }
     };
 
+    @Nullable private KeyguardBottomAreaViewBinder.Binding mBinding;
+    private boolean mUsesBinder;
+
     public KeyguardBottomAreaView(Context context) {
         this(context, null);
     }
@@ -142,13 +148,36 @@
         super(context, attrs, defStyleAttr, defStyleRes);
     }
 
-    /** Initializes the {@link KeyguardBottomAreaView} with the given dependencies */
+    /**
+     * Initializes the view.
+     */
+    public void init(
+            final KeyguardBottomAreaViewModel viewModel,
+            final FalsingManager falsingManager) {
+        Log.i(TAG, System.identityHashCode(this) + " initialized with a binder");
+        mUsesBinder = true;
+        mBinding = KeyguardBottomAreaViewBinder.bind(this, viewModel, falsingManager);
+    }
+
+    /**
+     * Initializes the {@link KeyguardBottomAreaView} with the given dependencies
+     *
+     * @deprecated Use
+     * {@link #init(KeyguardBottomAreaViewModel, FalsingManager)} instead
+     */
+    @Deprecated
     public void init(
             FalsingManager falsingManager,
             QuickAccessWalletController controller,
             ControlsComponent controlsComponent,
             QRCodeScannerController qrCodeScannerController) {
+        if (mUsesBinder) {
+            return;
+        }
+
+        Log.i(TAG, "initialized without a binder");
         mFalsingManager = falsingManager;
+
         mQuickAccessWalletController = controller;
         mQuickAccessWalletController.setupWalletChangeObservers(
                 mCardRetriever, WALLET_PREFERENCE_CHANGE, DEFAULT_PAYMENT_APP_CHANGE);
@@ -174,6 +203,10 @@
      * another {@link KeyguardBottomAreaView}
      */
     public void initFrom(KeyguardBottomAreaView oldBottomArea) {
+        if (mUsesBinder) {
+            return;
+        }
+
         // if it exists, continue to use the original ambient indication container
         // instead of the newly inflated one
         if (mAmbientIndicationArea != null) {
@@ -201,6 +234,10 @@
     @Override
     protected void onFinishInflate() {
         super.onFinishInflate();
+        if (mUsesBinder) {
+            return;
+        }
+
         mOverlayContainer = findViewById(R.id.overlay_container);
         mWalletButton = findViewById(R.id.wallet_button);
         mQRCodeScannerButton = findViewById(R.id.qr_code_scanner_button);
@@ -229,6 +266,10 @@
     @Override
     protected void onAttachedToWindow() {
         super.onAttachedToWindow();
+        if (mUsesBinder) {
+            return;
+        }
+
         final IntentFilter filter = new IntentFilter();
         filter.addAction(DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED);
         mKeyguardStateController.addCallback(mKeyguardStateCallback);
@@ -237,6 +278,10 @@
     @Override
     protected void onDetachedFromWindow() {
         super.onDetachedFromWindow();
+        if (mUsesBinder) {
+            return;
+        }
+
         mKeyguardStateController.removeCallback(mKeyguardStateCallback);
 
         if (mQuickAccessWalletController != null) {
@@ -259,6 +304,13 @@
     @Override
     protected void onConfigurationChanged(Configuration newConfig) {
         super.onConfigurationChanged(newConfig);
+        if (mUsesBinder) {
+            if (mBinding != null) {
+                mBinding.onConfigurationChanged();
+            }
+            return;
+        }
+
         mIndicationBottomMargin = getResources().getDimensionPixelSize(
                 R.dimen.keyguard_indication_margin_bottom);
         mBurnInYOffset = getResources().getDimensionPixelSize(
@@ -301,6 +353,10 @@
     }
 
     private void updateWalletVisibility() {
+        if (mUsesBinder) {
+            return;
+        }
+
         if (mDozing
                 || mQuickAccessWalletController == null
                 || !mQuickAccessWalletController.isWalletEnabled()
@@ -318,6 +374,10 @@
     }
 
     private void updateControlsVisibility() {
+        if (mUsesBinder) {
+            return;
+        }
+
         if (mControlsComponent == null) return;
 
         mControlsButton.setImageResource(mControlsComponent.getTileImageId());
@@ -344,6 +404,10 @@
     }
 
     public void setDarkAmount(float darkAmount) {
+        if (mUsesBinder) {
+            return;
+        }
+
         if (darkAmount == mDarkAmount) {
             return;
         }
@@ -355,6 +419,10 @@
      * Returns a list of animators to use to animate the indication areas.
      */
     public List<ViewPropertyAnimator> getIndicationAreaAnimators() {
+        if (mUsesBinder) {
+            return checkNotNull(mBinding).getIndicationAreaAnimators();
+        }
+
         List<ViewPropertyAnimator> animators =
                 new ArrayList<>(mAmbientIndicationArea != null ? 2 : 1);
         animators.add(mIndicationArea.animate());
@@ -394,6 +462,10 @@
     }
 
     public void setDozing(boolean dozing, boolean animate) {
+        if (mUsesBinder) {
+            return;
+        }
+
         mDozing = dozing;
 
         updateWalletVisibility();
@@ -411,6 +483,10 @@
     }
 
     public void dozeTimeTick() {
+        if (mUsesBinder) {
+            return;
+        }
+
         int burnInYOffset = getBurnInOffset(mBurnInYOffset * 2, false /* xAxis */)
                 - mBurnInYOffset;
         mIndicationArea.setTranslationY(burnInYOffset * mDarkAmount);
@@ -420,6 +496,10 @@
     }
 
     public void setAntiBurnInOffsetX(int burnInXOffset) {
+        if (mUsesBinder) {
+            return;
+        }
+
         if (mBurnInXOffset == burnInXOffset) {
             return;
         }
@@ -435,6 +515,10 @@
      * action buttons. Does not set the alpha of the lock icon.
      */
     public void setComponentAlphas(float alpha) {
+        if (mUsesBinder) {
+            return;
+        }
+
         setImportantForAccessibility(
                 alpha == 0f
                         ? View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
@@ -461,6 +545,10 @@
     }
 
     private void updateQRCodeButtonVisibility() {
+        if (mUsesBinder) {
+            return;
+        }
+
         if (mQuickAccessWalletController != null
                 && mQuickAccessWalletController.isWalletEnabled()) {
             // Don't enable if quick access wallet is enabled
@@ -481,6 +569,10 @@
     }
 
     private void onQRCodeScannerClicked(View view) {
+        if (mUsesBinder) {
+            return;
+        }
+
         Intent intent = mQRCodeScannerController.getIntent();
         if (intent != null) {
             try {
@@ -500,6 +592,10 @@
     }
 
     private void updateAffordanceColors() {
+        if (mUsesBinder) {
+            return;
+        }
+
         int iconColor = Utils.getColorAttrDefaultColor(
                 mContext,
                 com.android.internal.R.attr.textColorPrimary);
@@ -516,6 +612,10 @@
     }
 
     private void onWalletClick(View v) {
+        if (mUsesBinder) {
+            return;
+        }
+
         // More coming here; need to inform the user about how to proceed
         if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return;
@@ -531,6 +631,10 @@
     }
 
     private void onControlsClick(View v) {
+        if (mUsesBinder) {
+            return;
+        }
+
         if (mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
             return;
         }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
index 70ec13b14..4496607 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculator.kt
@@ -17,7 +17,6 @@
 package com.android.systemui.statusbar.phone
 
 import android.annotation.ColorInt
-import android.graphics.Color
 import android.graphics.Rect
 import android.view.InsetsFlags
 import android.view.ViewDebug
@@ -39,7 +38,13 @@
 class LetterboxAppearance(
     @Appearance val appearance: Int,
     val appearanceRegions: Array<AppearanceRegion>
-)
+) {
+    override fun toString(): String {
+        val appearanceString =
+                ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance)
+        return "LetterboxAppearance{$appearanceString, ${appearanceRegions.contentToString()}}"
+    }
+}
 
 /**
  * Responsible for calculating the [Appearance] and [AppearanceRegion] for the status bar when apps
@@ -51,6 +56,7 @@
 constructor(
     private val lightBarController: LightBarController,
     private val dumpManager: DumpManager,
+    private val letterboxBackgroundProvider: LetterboxBackgroundProvider,
 ) : OnStatusBarViewInitializedListener, CentralSurfacesComponent.Startable {
 
     private var statusBarBoundsProvider: StatusBarBoundsProvider? = null
@@ -184,13 +190,11 @@
 
     @ColorInt
     private fun outerLetterboxBackgroundColor(): Int {
-        // TODO(b/238607453): retrieve this information from WindowManager.
-        return Color.BLACK
+        return letterboxBackgroundProvider.letterboxBackgroundColor
     }
 
     private fun isOuterLetterboxMultiColored(): Boolean {
-        // TODO(b/238607453): retrieve this information from WindowManager.
-        return false
+        return letterboxBackgroundProvider.isLetterboxBackgroundMultiColored
     }
 
     private fun getEndSideIconsBounds(): Rect {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
new file mode 100644
index 0000000..96b9aca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProvider.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.statusbar.phone
+
+import android.annotation.ColorInt
+import android.graphics.Color
+import android.os.RemoteException
+import android.view.IWindowManager
+import com.android.systemui.Dumpable
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
+import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
+import java.io.PrintWriter
+import java.util.concurrent.Executor
+import javax.inject.Inject
+
+/** Responsible for providing information about the background of letterboxed apps. */
+@CentralSurfacesScope
+class LetterboxBackgroundProvider
+@Inject
+constructor(
+    private val windowManager: IWindowManager,
+    @Background private val backgroundExecutor: Executor,
+    private val dumpManager: DumpManager,
+) : CentralSurfacesComponent.Startable, Dumpable {
+
+    @ColorInt
+    var letterboxBackgroundColor: Int = Color.BLACK
+        private set
+
+    var isLetterboxBackgroundMultiColored: Boolean = false
+        private set
+
+    override fun start() {
+        dumpManager.registerDumpable(javaClass.simpleName, this)
+
+        // Using a background executor, as binder calls to IWindowManager are blocking
+        backgroundExecutor.execute {
+            try {
+                isLetterboxBackgroundMultiColored = windowManager.isLetterboxBackgroundMultiColored
+                letterboxBackgroundColor = windowManager.letterboxBackgroundColorInArgb
+            } catch (e: RemoteException) {
+                e.rethrowFromSystemServer()
+            }
+        }
+    }
+
+    override fun stop() {
+        dumpManager.unregisterDumpable(javaClass.simpleName)
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println(
+            """
+           letterboxBackgroundColor: ${Color.valueOf(letterboxBackgroundColor)}
+           isLetterboxBackgroundMultiColored: $isLetterboxBackgroundMultiColored
+       """.trimIndent())
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
index db8f4ee..6996ee7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java
@@ -41,7 +41,6 @@
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
-import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -50,7 +49,6 @@
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.AboveShelfObserver;
@@ -82,7 +80,6 @@
 
     private final ActivityStarter mActivityStarter;
     private final KeyguardStateController mKeyguardStateController;
-    private final NotificationViewHierarchyManager mViewHierarchyManager;
     private final NotificationLockscreenUserManager mLockscreenUserManager;
     private final SysuiStatusBarStateController mStatusBarStateController;
     private final NotifShadeEventSource mNotifShadeEventSource;
@@ -94,10 +91,8 @@
     private final HeadsUpManagerPhone mHeadsUpManager;
     private final AboveShelfObserver mAboveShelfObserver;
     private final DozeScrimController mDozeScrimController;
-    private final ScrimController mScrimController;
     private final KeyguardIndicationController mKeyguardIndicationController;
     private final CentralSurfaces mCentralSurfaces;
-    private final com.android.systemui.shade.ShadeController mShadeController;
     private final LockscreenShadeTransitionController mShadeTransitionController;
     private final CommandQueue mCommandQueue;
 
@@ -112,23 +107,21 @@
     protected boolean mVrMode;
 
     @Inject
-    StatusBarNotificationPresenter(Context context,
+    StatusBarNotificationPresenter(
+            Context context,
             NotificationPanelViewController panel,
             HeadsUpManagerPhone headsUp,
             NotificationShadeWindowView statusBarWindow,
             ActivityStarter activityStarter,
             NotificationStackScrollLayoutController stackScrollerController,
             DozeScrimController dozeScrimController,
-            ScrimController scrimController,
             NotificationShadeWindowController notificationShadeWindowController,
             DynamicPrivacyController dynamicPrivacyController,
             KeyguardStateController keyguardStateController,
             KeyguardIndicationController keyguardIndicationController,
             CentralSurfaces centralSurfaces,
-            ShadeController shadeController,
             LockscreenShadeTransitionController shadeTransitionController,
             CommandQueue commandQueue,
-            NotificationViewHierarchyManager notificationViewHierarchyManager,
             NotificationLockscreenUserManager lockscreenUserManager,
             SysuiStatusBarStateController sysuiStatusBarStateController,
             NotifShadeEventSource notifShadeEventSource,
@@ -149,10 +142,8 @@
         mKeyguardIndicationController = keyguardIndicationController;
         // TODO: use KeyguardStateController#isOccluded to remove this dependency
         mCentralSurfaces = centralSurfaces;
-        mShadeController = shadeController;
         mShadeTransitionController = shadeTransitionController;
         mCommandQueue = commandQueue;
-        mViewHierarchyManager = notificationViewHierarchyManager;
         mLockscreenUserManager = lockscreenUserManager;
         mStatusBarStateController = sysuiStatusBarStateController;
         mNotifShadeEventSource = notifShadeEventSource;
@@ -166,7 +157,6 @@
                 R.id.notification_container_parent));
         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
         mDozeScrimController = dozeScrimController;
-        mScrimController = scrimController;
         mKeyguardManager = context.getSystemService(KeyguardManager.class);
         mBarService = IStatusBarService.Stub.asInterface(
                 ServiceManager.getService(Context.STATUS_BAR_SERVICE));
@@ -187,16 +177,13 @@
 
         initController.addPostInitTask(() -> {
             mKeyguardIndicationController.init();
-            mViewHierarchyManager.setUpWithPresenter(this,
-                    stackScrollerController.getNotifStackController(),
-                    mNotifListContainer);
             mNotifShadeEventSource.setShadeEmptiedCallback(this::maybeClosePanelForShadeEmptied);
             mNotifShadeEventSource.setNotifRemovedByUserCallback(this::maybeEndAmbientPulse);
             notificationInterruptStateProvider.addSuppressor(mInterruptSuppressor);
             mLockscreenUserManager.setUpWithPresenter(this);
             mMediaManager.setUpWithPresenter(this);
             mGutsManager.setUpWithPresenter(
-                    this, mNotifListContainer, mCheckSaveListener, mOnSettingsClickListener);
+                    this, mNotifListContainer, mOnSettingsClickListener);
             // ForegroundServiceNotificationListener adds its listener in its constructor
             // but we need to request it here in order for it to be instantiated.
             // TODO: figure out how to do this correctly once Dependency.get() is gone.
@@ -233,24 +220,6 @@
     }
 
     @Override
-    public void updateNotificationViews(final String reason) {
-        if (!mNotifPipelineFlags.checkLegacyPipelineEnabled()) {
-            return;
-        }
-        // The function updateRowStates depends on both of these being non-null, so check them here.
-        // We may be called before they are set from DeviceProvisionedController's callback.
-        if (mScrimController == null) return;
-
-        // Do not modify the notifications during collapse.
-        if (isCollapsing()) {
-            mShadeController.addPostCollapseAction(() -> updateNotificationViews(reason));
-            return;
-        }
-        mViewHierarchyManager.updateNotificationViews();
-        mNotificationPanel.updateNotificationViews(reason);
-    }
-
-    @Override
     public void onUserSwitched(int newUserId) {
         // Begin old BaseStatusBar.userSwitched
         mHeadsUpManager.setUser(newUserId);
@@ -304,11 +273,6 @@
     }
 
     @Override
-    public void onUpdateRowStates() {
-        mNotificationPanel.onUpdateRowStates();
-    }
-
-    @Override
     public void onExpandClicked(NotificationEntry clickedEntry, View clickedView,
             boolean nowExpanded) {
         mHeadsUpManager.setExpanded(clickedEntry, nowExpanded);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java
index d57e6a7..b0532d7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.phone.dagger;
 
 import com.android.systemui.statusbar.phone.LetterboxAppearanceCalculator;
+import com.android.systemui.statusbar.phone.LetterboxBackgroundProvider;
 import com.android.systemui.statusbar.phone.SystemBarAttributesListener;
 
 import java.util.Set;
@@ -40,4 +41,9 @@
     @IntoSet
     CentralSurfacesComponent.Startable sysBarAttrsListener(
             SystemBarAttributesListener systemBarAttributesListener);
+
+    @Binds
+    @IntoSet
+    CentralSurfacesComponent.Startable letterboxBgProvider(
+            LetterboxBackgroundProvider letterboxBackgroundProvider);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
index 0848729..fb26600 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragment.java
@@ -439,7 +439,6 @@
             state |= DISABLE_CLOCK;
         }
 
-
         if (mNetworkController != null && EncryptionHelper.IS_DATA_ENCRYPTED) {
             if (mNetworkController.hasEmergencyCryptKeeperText()) {
                 state |= DISABLE_NOTIFICATION_ICONS;
@@ -449,13 +448,6 @@
             }
         }
 
-        // The shelf will be hidden when dozing with a custom clock, we must show notification
-        // icons in this occasion.
-        if (mStatusBarStateController.isDozing()
-                && mNotificationPanelViewController.hasCustomClock()) {
-            state |= DISABLE_CLOCK | DISABLE_SYSTEM_INFO;
-        }
-
         if (mOngoingCallController.hasOngoingCall()) {
             state &= ~DISABLE_ONGOING_CALL_CHIP;
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
index bd6cf9a..a841914 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityInfoProcessor.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.pipeline
 
+import android.content.Context
+import com.android.systemui.CoreStartable
 import com.android.systemui.dagger.SysUISingleton
 import javax.inject.Inject
 
@@ -27,4 +29,18 @@
  * displayed in the RHS of the status bar.
  */
 @SysUISingleton
-class ConnectivityInfoProcessor @Inject constructor()
+class ConnectivityInfoProcessor @Inject constructor(
+        context: Context,
+        private val statusBarPipelineFlags: StatusBarPipelineFlags,
+) : CoreStartable(context) {
+    override fun start() {
+        if (statusBarPipelineFlags.isNewPipelineEnabled()) {
+            init()
+        }
+    }
+
+    /** Initializes this processor and everything it depends on. */
+    private fun init() {
+        // TODO(b/238425913): Register all the connectivity callbacks here.
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLogger.kt
new file mode 100644
index 0000000..f88e9d6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLogger.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.statusbar.pipeline
+
+import android.net.Network
+import android.net.NetworkCapabilities
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.log.LogBuffer
+import com.android.systemui.log.LogLevel
+import com.android.systemui.log.dagger.StatusBarConnectivityLog
+import javax.inject.Inject
+
+@SysUISingleton
+class ConnectivityPipelineLogger @Inject constructor(
+    @StatusBarConnectivityLog private val buffer: LogBuffer,
+) {
+    fun logOnCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = network.getNetId()
+                str1 = networkCapabilities.toString()
+            },
+            {
+                "onCapabilitiesChanged: net=$int1 capabilities=$str1"
+            }
+        )
+    }
+
+    fun logOnLost(network: Network) {
+        buffer.log(
+            TAG,
+            LogLevel.INFO,
+            {
+                int1 = network.getNetId()
+            },
+            {
+                "onLost: net=$int1"
+            }
+        )
+    }
+}
+
+private const val TAG = "SbConnectivityPipeline"
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
new file mode 100644
index 0000000..589cdb8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.statusbar.pipeline
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import javax.inject.Inject
+
+/** All flagging methods related to the new status bar pipeline (see b/238425913). */
+@SysUISingleton
+class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) {
+    /**
+     * Returns true if we should run the new pipeline.
+     *
+     * TODO(b/238425913): We may want to split this out into:
+     *   (1) isNewPipelineLoggingEnabled(), where the new pipeline runs and logs its decisions but
+     *       doesn't change the UI at all.
+     *   (2) isNewPipelineEnabled(), where the new pipeline runs and does change the UI (and the old
+     *       pipeline doesn't change the UI).
+     */
+    fun isNewPipelineEnabled(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 734bd2d..771bb0c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -16,27 +16,18 @@
 
 package com.android.systemui.statusbar.pipeline.dagger
 
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.flags.FeatureFlags
-import com.android.systemui.flags.Flags
+import com.android.systemui.CoreStartable
 import com.android.systemui.statusbar.pipeline.ConnectivityInfoProcessor
-import dagger.Lazy
+import dagger.Binds
 import dagger.Module
-import dagger.Provides
-import java.util.Optional
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
 
 @Module
-class StatusBarPipelineModule {
-    @Provides
-    @SysUISingleton
-    fun provideConnectivityInfoProcessor(
-            featureFlags: FeatureFlags,
-            processorLazy: Lazy<ConnectivityInfoProcessor>
-    ): Optional<ConnectivityInfoProcessor> {
-        return if (featureFlags.isEnabled(Flags.NEW_STATUS_BAR_PIPELINE)) {
-            Optional.of(processorLazy.get())
-        } else {
-            Optional.empty()
-        }
-    }
+abstract class StatusBarPipelineModule {
+    /** Inject into ConnectivityInfoProcessor. */
+    @Binds
+    @IntoMap
+    @ClassKey(ConnectivityInfoProcessor::class)
+    abstract fun bindConnectivityInfoProcessor(cip: ConnectivityInfoProcessor): CoreStartable
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepo.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepo.kt
new file mode 100644
index 0000000..e5980c3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepo.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2021 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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.statusbar.pipeline.repository
+
+import android.annotation.SuppressLint
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkRequest
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.statusbar.pipeline.ConnectivityPipelineLogger
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted.Companion.Lazily
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.stateIn
+
+/** Repository that contains all relevant [NetworkCapabilites] for the current networks */
+@SysUISingleton
+class NetworkCapabilitiesRepo @Inject constructor(
+    connectivityManager: ConnectivityManager,
+    @Application scope: CoroutineScope,
+    logger: ConnectivityPipelineLogger,
+) {
+    @SuppressLint("MissingPermission")
+    val dataStream: StateFlow<Map<Int, NetworkCapabilityInfo>> = run {
+        var state = emptyMap<Int, NetworkCapabilityInfo>()
+        callbackFlow {
+                val networkRequest: NetworkRequest =
+                    NetworkRequest.Builder()
+                        .clearCapabilities()
+                        .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+                        .build()
+                val callback =
+                    // TODO (b/240569788): log these using [LogBuffer]
+                    object : ConnectivityManager.NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) {
+                        override fun onCapabilitiesChanged(
+                            network: Network,
+                            networkCapabilities: NetworkCapabilities
+                        ) {
+                            logger.logOnCapabilitiesChanged(network, networkCapabilities)
+                            state =
+                                state.toMutableMap().also {
+                                    it[network.getNetId()] =
+                                        NetworkCapabilityInfo(network, networkCapabilities)
+                                }
+                            trySend(state)
+                        }
+
+                        override fun onLost(network: Network) {
+                            logger.logOnLost(network)
+                            state = state.toMutableMap().also { it.remove(network.getNetId()) }
+                            trySend(state)
+                        }
+                    }
+                connectivityManager.registerNetworkCallback(networkRequest, callback)
+
+                awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
+            }
+            .stateIn(scope, started = Lazily, initialValue = state)
+    }
+}
+
+/** contains info about network capabilities. */
+data class NetworkCapabilityInfo(
+    val network: Network,
+    val capabilities: NetworkCapabilities,
+)
+
+private const val TAG = "ConnectivityRepository"
diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
index 3e07144..e22a896 100644
--- a/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
+++ b/packages/SystemUI/src/com/android/systemui/wmshell/BubblesManager.java
@@ -40,7 +40,6 @@
 import android.provider.Settings;
 import android.service.notification.NotificationListenerService.RankingMap;
 import android.service.notification.ZenModeConfig;
-import android.util.ArraySet;
 import android.util.Log;
 import android.util.Pair;
 import android.util.SparseArray;
@@ -83,6 +82,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.Executor;
 import java.util.function.Consumer;
 import java.util.function.IntConsumer;
@@ -262,7 +262,7 @@
             }
 
             @Override
-            public void getShouldRestoredEntries(ArraySet<String> savedBubbleKeys,
+            public void getShouldRestoredEntries(Set<String> savedBubbleKeys,
                     Consumer<List<BubbleEntry>> callback) {
                 sysuiMainExecutor.execute(() -> {
                     List<BubbleEntry> result = new ArrayList<>();
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
new file mode 100644
index 0000000..4f39952
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.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.keyguard
+
+import android.content.BroadcastReceiver
+import android.testing.AndroidTestingRunner
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.plugins.Clock
+import com.android.systemui.plugins.ClockAnimations
+import com.android.systemui.plugins.ClockEvents
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.policy.BatteryController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.capture
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import java.util.TimeZone
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyFloat
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.junit.MockitoJUnit
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class ClockEventControllerTest : SysuiTestCase() {
+
+    @JvmField @Rule val mockito = MockitoJUnit.rule()
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
+    @Mock private lateinit var batteryController: BatteryController
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock private lateinit var configurationController: ConfigurationController
+    @Mock private lateinit var animations: ClockAnimations
+    @Mock private lateinit var events: ClockEvents
+    @Mock private lateinit var clock: Clock
+
+    private lateinit var clockEventController: ClockEventController
+
+    @Before
+    fun setUp() {
+        whenever(clock.smallClock).thenReturn(TextView(context))
+        whenever(clock.largeClock).thenReturn(TextView(context))
+        whenever(clock.events).thenReturn(events)
+        whenever(clock.animations).thenReturn(animations)
+
+        clockEventController = ClockEventController(
+            statusBarStateController,
+            broadcastDispatcher,
+            batteryController,
+            keyguardUpdateMonitor,
+            configurationController,
+            context.resources,
+            context
+        )
+    }
+
+    @Test
+    fun clockSet_validateInitialization() {
+        clockEventController.clock = clock
+
+        verify(clock).initialize(any(), anyFloat(), anyFloat())
+    }
+
+    @Test
+    fun clockUnset_validateState() {
+        clockEventController.clock = clock
+        clockEventController.clock = null
+
+        assertEquals(clockEventController.clock, null)
+    }
+
+    @Test
+    fun themeChanged_verifyClockPaletteUpdated() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val captor = argumentCaptor<ConfigurationController.ConfigurationListener>()
+        verify(configurationController).addCallback(capture(captor))
+        captor.value.onThemeChanged()
+
+        verify(events).onColorPaletteChanged(any())
+    }
+
+    @Test
+    fun batteryCallback_keyguardShowingCharging_verifyChargeAnimation() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+        verify(batteryController).addCallback(capture(batteryCaptor))
+        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+        keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
+
+        verify(animations).charge()
+    }
+
+    @Test
+    fun batteryCallback_keyguardShowingCharging_Duplicate_verifyChargeAnimation() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+        verify(batteryController).addCallback(capture(batteryCaptor))
+        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+        keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
+        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
+
+        verify(animations, times(1)).charge()
+    }
+
+    @Test
+    fun batteryCallback_keyguardHiddenCharging_verifyChargeAnimation() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+        verify(batteryController).addCallback(capture(batteryCaptor))
+        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+        keyguardCaptor.value.onKeyguardVisibilityChanged(false)
+        batteryCaptor.value.onBatteryLevelChanged(10, false, true)
+
+        verify(animations, never()).charge()
+    }
+
+    @Test
+    fun batteryCallback_keyguardShowingNotCharging_verifyChargeAnimation() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val batteryCaptor = argumentCaptor<BatteryController.BatteryStateChangeCallback>()
+        verify(batteryController).addCallback(capture(batteryCaptor))
+        val keyguardCaptor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCaptor))
+        keyguardCaptor.value.onKeyguardVisibilityChanged(true)
+        batteryCaptor.value.onBatteryLevelChanged(10, false, false)
+
+        verify(animations, never()).charge()
+    }
+
+    @Test
+    fun localeCallback_verifyClockNotified() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val captor = argumentCaptor<BroadcastReceiver>()
+        verify(broadcastDispatcher).registerReceiver(
+            capture(captor), any(), eq(null), eq(null), anyInt(), eq(null)
+        )
+        captor.value.onReceive(context, mock())
+
+        verify(events).onLocaleChanged(any())
+    }
+
+    @Test
+    fun keyguardCallback_visibilityChanged_clockDozeCalled() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(capture(captor))
+
+        captor.value.onKeyguardVisibilityChanged(true)
+        verify(animations, never()).doze(0f)
+
+        captor.value.onKeyguardVisibilityChanged(false)
+        verify(animations, times(1)).doze(0f)
+    }
+
+    @Test
+    fun keyguardCallback_timeFormat_clockNotified() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(capture(captor))
+        captor.value.onTimeFormatChanged("12h")
+
+        verify(events).onTimeFormatChanged(false)
+    }
+
+    @Test
+    fun keyguardCallback_timezoneChanged_clockNotified() {
+        val mockTimeZone = mock<TimeZone>()
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(capture(captor))
+        captor.value.onTimeZoneChanged(mockTimeZone)
+
+        verify(events).onTimeZoneChanged(mockTimeZone)
+    }
+
+    @Test
+    fun keyguardCallback_userSwitched_clockNotified() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val captor = argumentCaptor<KeyguardUpdateMonitorCallback>()
+        verify(keyguardUpdateMonitor).registerCallback(capture(captor))
+        captor.value.onUserSwitchComplete(10)
+
+        verify(events).onTimeFormatChanged(false)
+    }
+
+    @Test
+    fun keyguardCallback_verifyKeyguardChanged() {
+        clockEventController.clock = clock
+        clockEventController.registerListeners()
+
+        val captor = argumentCaptor<StatusBarStateController.StateListener>()
+        verify(statusBarStateController).addCallback(capture(captor))
+        captor.value.onDozeAmountChanged(0.4f, 0.6f)
+
+        verify(animations).doze(0.4f)
+    }
+
+    @Test
+    fun unregisterListeners_validate() {
+        clockEventController.unregisterListeners()
+        verify(broadcastDispatcher).unregisterReceiver(any())
+        verify(configurationController).removeCallback(any())
+        verify(batteryController).removeCallback(any())
+        verify(keyguardUpdateMonitor).removeCallback(any())
+        verify(statusBarStateController).removeCallback(any())
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
index 8b9a1e0..635ee9e 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchControllerTest.java
@@ -19,7 +19,6 @@
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
-import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
@@ -41,23 +40,19 @@
 
 import androidx.test.filters.SmallTest;
 
-import com.android.internal.colorextraction.ColorExtractor;
-import com.android.keyguard.clock.ClockManager;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.broadcast.BroadcastDispatcher;
-import com.android.systemui.colorextraction.SysuiColorExtractor;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
-import com.android.systemui.plugins.ClockPlugin;
+import com.android.systemui.plugins.Clock;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shared.clocks.AnimatableClockView;
+import com.android.systemui.shared.clocks.ClockRegistry;
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController;
-import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationIconAreaController;
 import com.android.systemui.statusbar.phone.NotificationIconContainer;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.util.concurrency.FakeExecutor;
 import com.android.systemui.util.settings.SecureSettings;
 import com.android.systemui.util.time.FakeSystemClock;
@@ -79,22 +74,12 @@
     @Mock
     private StatusBarStateController mStatusBarStateController;
     @Mock
-    private SysuiColorExtractor mColorExtractor;
-    @Mock
-    private ClockManager mClockManager;
+    private ClockRegistry mClockRegistry;
     @Mock
     KeyguardSliceViewController mKeyguardSliceViewController;
     @Mock
     NotificationIconAreaController mNotificationIconAreaController;
     @Mock
-    BroadcastDispatcher mBroadcastDispatcher;
-    @Mock
-    BatteryController mBatteryController;
-    @Mock
-    KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @Mock
-    KeyguardBypassController mBypassController;
-    @Mock
     LockscreenSmartspaceController mSmartspaceController;
 
     @Mock
@@ -102,11 +87,11 @@
     @Mock
     KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     @Mock
-    private ClockPlugin mClockPlugin;
-    @Mock
-    ColorExtractor.GradientColors mGradientColors;
+    private Clock mClock;
     @Mock
     DumpManager mDumpManager;
+    @Mock
+    ClockEventController mClockEventController;
 
     @Mock
     private NotificationIconContainer mNotificationIcons;
@@ -118,6 +103,8 @@
     private FrameLayout mLargeClockFrame;
     @Mock
     private SecureSettings mSecureSettings;
+    @Mock
+    private FeatureFlags mFeatureFlags;
 
     private final View mFakeSmartspaceView = new View(mContext);
 
@@ -136,8 +123,6 @@
         when(mView.getContext()).thenReturn(getContext());
         when(mView.getResources()).thenReturn(mResources);
 
-        when(mView.findViewById(R.id.animatable_clock_view)).thenReturn(mClockView);
-        when(mView.findViewById(R.id.animatable_clock_view_large)).thenReturn(mLargeClockView);
         when(mView.findViewById(R.id.lockscreen_clock_view_large)).thenReturn(mLargeClockFrame);
         when(mClockView.getContext()).thenReturn(getContext());
         when(mLargeClockView.getContext()).thenReturn(getContext());
@@ -148,23 +133,20 @@
         mController = new KeyguardClockSwitchController(
                 mView,
                 mStatusBarStateController,
-                mColorExtractor,
-                mClockManager,
+                mClockRegistry,
                 mKeyguardSliceViewController,
                 mNotificationIconAreaController,
-                mBroadcastDispatcher,
-                mBatteryController,
-                mKeyguardUpdateMonitor,
                 mSmartspaceController,
                 mKeyguardUnlockAnimationController,
                 mSecureSettings,
                 mExecutor,
-                mResources,
-                mDumpManager
+                mDumpManager,
+                mClockEventController,
+                mFeatureFlags
         );
 
         when(mStatusBarStateController.getState()).thenReturn(StatusBarState.SHADE);
-        when(mColorExtractor.getColors(anyInt())).thenReturn(mGradientColors);
+        when(mClockRegistry.createCurrentClock()).thenReturn(mClock);
 
         mSliceView = new View(getContext());
         when(mView.findViewById(R.id.keyguard_slice_view)).thenReturn(mSliceView);
@@ -211,20 +193,20 @@
         verifyAttachment(times(1));
 
         listenerArgumentCaptor.getValue().onViewDetachedFromWindow(mView);
-        verify(mColorExtractor).removeOnColorsChangedListener(
-                any(ColorExtractor.OnColorsChangedListener.class));
+        verify(mClockEventController).unregisterListeners();
     }
 
     @Test
     public void testPluginPassesStatusBarState() {
-        ArgumentCaptor<ClockManager.ClockChangedListener> listenerArgumentCaptor =
-                ArgumentCaptor.forClass(ClockManager.ClockChangedListener.class);
+        ArgumentCaptor<ClockRegistry.ClockChangeListener> listenerArgumentCaptor =
+                ArgumentCaptor.forClass(ClockRegistry.ClockChangeListener.class);
 
         mController.init();
-        verify(mClockManager).addOnClockChangedListener(listenerArgumentCaptor.capture());
+        verify(mClockRegistry).registerClockChangeListener(listenerArgumentCaptor.capture());
 
-        listenerArgumentCaptor.getValue().onClockChanged(mClockPlugin);
-        verify(mView).setClockPlugin(mClockPlugin, StatusBarState.SHADE);
+        listenerArgumentCaptor.getValue().onClockChanged();
+        verify(mView, times(2)).setClock(mClock, StatusBarState.SHADE);
+        verify(mClockEventController, times(2)).setClock(mClock);
     }
 
     @Test
@@ -281,10 +263,8 @@
     }
 
     private void verifyAttachment(VerificationMode times) {
-        verify(mClockManager, times).addOnClockChangedListener(
-                any(ClockManager.ClockChangedListener.class));
-        verify(mColorExtractor, times).addOnColorsChangedListener(
-                any(ColorExtractor.OnColorsChangedListener.class));
-        verify(mView, times).updateColors(mGradientColors);
+        verify(mClockRegistry, times).registerClockChangeListener(
+                any(ClockRegistry.ClockChangeListener.class));
+        verify(mClockEventController, times).registerListeners();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
index 6c6f0ac..a0295d0 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardClockSwitchTest.java
@@ -16,7 +16,6 @@
 
 package com.android.keyguard;
 
-import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
 import static com.android.keyguard.KeyguardClockSwitch.LARGE;
@@ -24,56 +23,61 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
+import static junit.framework.TestCase.assertEquals;
+
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.content.Context;
-import android.graphics.Color;
-import android.graphics.Paint.Style;
 import android.test.suitebuilder.annotation.SmallTest;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 import android.util.AttributeSet;
 import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.FrameLayout;
-import android.widget.TextClock;
+import android.widget.TextView;
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.plugins.ClockPlugin;
-import com.android.systemui.shared.clocks.AnimatableClockView;
+import com.android.systemui.plugins.Clock;
 import com.android.systemui.statusbar.StatusBarState;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 // Need to run on the main thread because KeyguardSliceView$Row init checks for
 // the main thread before acquiring a wake lock. This class is constructed when
-// the keyguard_clcok_switch layout is inflated.
+// the keyguard_clock_switch layout is inflated.
 @RunWithLooper(setAsMainLooper = true)
 public class KeyguardClockSwitchTest extends SysuiTestCase {
-    private FrameLayout mClockFrame;
-    private FrameLayout mLargeClockFrame;
-    private TextClock mBigClock;
+    @Mock
+    ViewGroup mMockKeyguardSliceView;
 
-    private AnimatableClockView mClockView;
-    private AnimatableClockView mLargeClockView;
-    View mMockKeyguardSliceView;
+    @Mock
+    Clock mClock;
+
+    private FrameLayout mSmallClockFrame;
+    private FrameLayout mLargeClockFrame;
+
     KeyguardClockSwitch mKeyguardClockSwitch;
 
     @Before
     public void setUp() {
-        mMockKeyguardSliceView = mock(KeyguardSliceView.class);
+        MockitoAnnotations.initMocks(this);
         when(mMockKeyguardSliceView.getContext()).thenReturn(mContext);
         when(mMockKeyguardSliceView.findViewById(R.id.keyguard_status_area))
                 .thenReturn(mMockKeyguardSliceView);
 
+        when(mClock.getSmallClock()).thenReturn(new TextView(getContext()));
+        when(mClock.getLargeClock()).thenReturn(new TextView(getContext()));
+
         LayoutInflater layoutInflater = LayoutInflater.from(getContext());
         layoutInflater.setPrivateFactory(new LayoutInflater.Factory2() {
 
@@ -93,164 +97,68 @@
         });
         mKeyguardClockSwitch =
                 (KeyguardClockSwitch) layoutInflater.inflate(R.layout.keyguard_clock_switch, null);
-        mClockFrame = mKeyguardClockSwitch.findViewById(R.id.lockscreen_clock_view);
-        mClockView = mKeyguardClockSwitch.findViewById(R.id.animatable_clock_view);
+        mSmallClockFrame = mKeyguardClockSwitch.findViewById(R.id.lockscreen_clock_view);
         mLargeClockFrame = mKeyguardClockSwitch.findViewById(R.id.lockscreen_clock_view_large);
-        mLargeClockView = mKeyguardClockSwitch.findViewById(R.id.animatable_clock_view_large);
-        mBigClock = new TextClock(getContext());
         mKeyguardClockSwitch.mChildrenAreLaidOut = true;
-        MockitoAnnotations.initMocks(this);
     }
 
     @Test
-    public void onPluginConnected_showPluginClock() {
-        ClockPlugin plugin = mock(ClockPlugin.class);
-        TextClock pluginView = new TextClock(getContext());
-        when(plugin.getView()).thenReturn(pluginView);
-
-        mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD);
-
-        assertThat(mClockView.getVisibility()).isEqualTo(GONE);
-        assertThat(plugin.getView().getParent()).isEqualTo(mClockFrame);
+    public void noPluginConnected_showNothing() {
+        mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD);
+        assertEquals(mLargeClockFrame.getChildCount(), 0);
+        assertEquals(mSmallClockFrame.getChildCount(), 0);
     }
 
     @Test
-    public void onPluginConnected_showPluginBigClock() {
-        // GIVEN the plugin returns a view for the big clock
-        ClockPlugin plugin = mock(ClockPlugin.class);
-        when(plugin.getBigClockView()).thenReturn(mBigClock);
-        // WHEN the plugin is connected
-        mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD);
-        // THEN the big clock container is visible and it is the parent of the
-        // big clock view.
-        assertThat(mLargeClockView.getVisibility()).isEqualTo(View.GONE);
-        assertThat(mBigClock.getParent()).isEqualTo(mLargeClockFrame);
+    public void pluginConnectedThenDisconnected_showNothing() {
+        mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD);
+        assertEquals(mLargeClockFrame.getChildCount(), 1);
+        assertEquals(mSmallClockFrame.getChildCount(), 1);
+
+        mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD);
+        assertEquals(mLargeClockFrame.getChildCount(), 0);
+        assertEquals(mSmallClockFrame.getChildCount(), 0);
     }
 
     @Test
-    public void onPluginConnected_nullView() {
-        ClockPlugin plugin = mock(ClockPlugin.class);
-        mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD);
-        assertThat(mClockView.getVisibility()).isEqualTo(VISIBLE);
+    public void onPluginConnected_showClock() {
+        mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD);
+
+        assertEquals(mClock.getSmallClock().getParent(), mSmallClockFrame);
+        assertEquals(mClock.getLargeClock().getParent(), mLargeClockFrame);
     }
 
     @Test
     public void onPluginConnected_showSecondPluginClock() {
         // GIVEN a plugin has already connected
-        ClockPlugin plugin1 = mock(ClockPlugin.class);
-        when(plugin1.getView()).thenReturn(new TextClock(getContext()));
-        mKeyguardClockSwitch.setClockPlugin(plugin1, StatusBarState.KEYGUARD);
-        // WHEN a second plugin is connected
-        ClockPlugin plugin2 = mock(ClockPlugin.class);
-        when(plugin2.getView()).thenReturn(new TextClock(getContext()));
-        mKeyguardClockSwitch.setClockPlugin(plugin2, StatusBarState.KEYGUARD);
+        Clock otherClock = mock(Clock.class);
+        when(otherClock.getSmallClock()).thenReturn(new TextView(getContext()));
+        when(otherClock.getLargeClock()).thenReturn(new TextView(getContext()));
+        mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD);
+        mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD);
+
         // THEN only the view from the second plugin should be a child of KeyguardClockSwitch.
-        assertThat(plugin2.getView().getParent()).isEqualTo(mClockFrame);
-        assertThat(plugin1.getView().getParent()).isNull();
-    }
-
-    @Test
-    public void onPluginConnected_darkAmountInitialized() {
-        // GIVEN that the dark amount has already been set
-        mKeyguardClockSwitch.setDarkAmount(0.5f);
-        // WHEN a plugin is connected
-        ClockPlugin plugin = mock(ClockPlugin.class);
-        mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD);
-        // THEN dark amount should be initalized on the plugin.
-        verify(plugin).setDarkAmount(0.5f);
-    }
-
-    @Test
-    public void onPluginDisconnected_showDefaultClock() {
-        ClockPlugin plugin = mock(ClockPlugin.class);
-        TextClock pluginView = new TextClock(getContext());
-        when(plugin.getView()).thenReturn(pluginView);
-
-        mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD);
-        assertThat(mClockView.getVisibility()).isEqualTo(GONE);
-
-        mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD);
-        assertThat(mClockView.getVisibility()).isEqualTo(VISIBLE);
-
-        assertThat(plugin.getView().getParent()).isNull();
-    }
-
-    @Test
-    public void onPluginDisconnected_hidePluginBigClock() {
-        // GIVEN the plugin returns a view for the big clock
-        ClockPlugin plugin = mock(ClockPlugin.class);
-        TextClock pluginView = new TextClock(getContext());
-        when(plugin.getBigClockView()).thenReturn(pluginView);
-        // WHEN the plugin is connected and then disconnected
-        mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD);
-        mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD);
-        // THEN the big lock container is GONE and the big clock view doesn't have
-        // a parent.
-        assertThat(mLargeClockView.getVisibility()).isEqualTo(VISIBLE);
-        assertThat(pluginView.getParent()).isNull();
-    }
-
-    @Test
-    public void onPluginDisconnected_nullView() {
-        ClockPlugin plugin = mock(ClockPlugin.class);
-        mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD);
-        mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD);
-        assertThat(mClockView.getVisibility()).isEqualTo(VISIBLE);
+        assertThat(otherClock.getSmallClock().getParent()).isEqualTo(mSmallClockFrame);
+        assertThat(otherClock.getLargeClock().getParent()).isEqualTo(mLargeClockFrame);
+        assertThat(mClock.getSmallClock().getParent()).isNull();
+        assertThat(mClock.getLargeClock().getParent()).isNull();
     }
 
     @Test
     public void onPluginDisconnected_secondOfTwoDisconnected() {
         // GIVEN two plugins are connected
-        ClockPlugin plugin1 = mock(ClockPlugin.class);
-        when(plugin1.getView()).thenReturn(new TextClock(getContext()));
-        mKeyguardClockSwitch.setClockPlugin(plugin1, StatusBarState.KEYGUARD);
-        ClockPlugin plugin2 = mock(ClockPlugin.class);
-        when(plugin2.getView()).thenReturn(new TextClock(getContext()));
-        mKeyguardClockSwitch.setClockPlugin(plugin2, StatusBarState.KEYGUARD);
+        Clock otherClock = mock(Clock.class);
+        when(otherClock.getSmallClock()).thenReturn(new TextView(getContext()));
+        when(otherClock.getLargeClock()).thenReturn(new TextView(getContext()));
+        mKeyguardClockSwitch.setClock(otherClock, StatusBarState.KEYGUARD);
+        mKeyguardClockSwitch.setClock(mClock, StatusBarState.KEYGUARD);
         // WHEN the second plugin is disconnected
-        mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD);
-        // THEN the default clock should be shown.
-        assertThat(mClockView.getVisibility()).isEqualTo(VISIBLE);
-        assertThat(plugin1.getView().getParent()).isNull();
-        assertThat(plugin2.getView().getParent()).isNull();
-    }
-
-    @Test
-    public void onPluginDisconnected_onDestroyView() {
-        // GIVEN a plugin is connected
-        ClockPlugin clockPlugin = mock(ClockPlugin.class);
-        when(clockPlugin.getView()).thenReturn(new TextClock(getContext()));
-        mKeyguardClockSwitch.setClockPlugin(clockPlugin, StatusBarState.KEYGUARD);
-        // WHEN the plugin is disconnected
-        mKeyguardClockSwitch.setClockPlugin(null, StatusBarState.KEYGUARD);
-        // THEN onDestroyView is called on the plugin
-        verify(clockPlugin).onDestroyView();
-    }
-
-    @Test
-    public void setTextColor_pluginClockSetTextColor() {
-        ClockPlugin plugin = mock(ClockPlugin.class);
-        TextClock pluginView = new TextClock(getContext());
-        when(plugin.getView()).thenReturn(pluginView);
-        mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD);
-
-        mKeyguardClockSwitch.setTextColor(Color.WHITE);
-
-        verify(plugin).setTextColor(Color.WHITE);
-    }
-
-
-    @Test
-    public void setStyle_pluginClockSetStyle() {
-        ClockPlugin plugin = mock(ClockPlugin.class);
-        TextClock pluginView = new TextClock(getContext());
-        when(plugin.getView()).thenReturn(pluginView);
-        Style style = mock(Style.class);
-        mKeyguardClockSwitch.setClockPlugin(plugin, StatusBarState.KEYGUARD);
-
-        mKeyguardClockSwitch.setStyle(style);
-
-        verify(plugin).setStyle(style);
+        mKeyguardClockSwitch.setClock(null, StatusBarState.KEYGUARD);
+        // THEN nothing should be shown
+        assertThat(otherClock.getSmallClock().getParent()).isNull();
+        assertThat(otherClock.getLargeClock().getParent()).isNull();
+        assertThat(mClock.getSmallClock().getParent()).isNull();
+        assertThat(mClock.getLargeClock().getParent()).isNull();
     }
 
     @Test
@@ -262,7 +170,7 @@
 
         assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
         assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
-        assertThat(mClockFrame.getAlpha()).isEqualTo(0);
+        assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
     }
 
     @Test
@@ -271,7 +179,7 @@
 
         assertThat(mLargeClockFrame.getAlpha()).isEqualTo(1);
         assertThat(mLargeClockFrame.getVisibility()).isEqualTo(VISIBLE);
-        assertThat(mClockFrame.getAlpha()).isEqualTo(0);
+        assertThat(mSmallClockFrame.getAlpha()).isEqualTo(0);
     }
 
     @Test
@@ -281,8 +189,8 @@
         mKeyguardClockSwitch.mClockInAnim.end();
         mKeyguardClockSwitch.mClockOutAnim.end();
 
-        assertThat(mClockFrame.getAlpha()).isEqualTo(1);
-        assertThat(mClockFrame.getVisibility()).isEqualTo(VISIBLE);
+        assertThat(mSmallClockFrame.getAlpha()).isEqualTo(1);
+        assertThat(mSmallClockFrame.getVisibility()).isEqualTo(VISIBLE);
         // only big clock is removed at switch
         assertThat(mLargeClockFrame.getParent()).isNull();
         assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
@@ -292,8 +200,8 @@
     public void switchingToSmallClockNoAnimation_makesBigClockDisappear() {
         mKeyguardClockSwitch.switchToClock(SMALL, false);
 
-        assertThat(mClockFrame.getAlpha()).isEqualTo(1);
-        assertThat(mClockFrame.getVisibility()).isEqualTo(VISIBLE);
+        assertThat(mSmallClockFrame.getAlpha()).isEqualTo(1);
+        assertThat(mSmallClockFrame.getVisibility()).isEqualTo(VISIBLE);
         // only big clock is removed at switch
         assertThat(mLargeClockFrame.getParent()).isNull();
         assertThat(mLargeClockFrame.getAlpha()).isEqualTo(0);
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 68e49c0..dc87a6a 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -318,7 +318,7 @@
     @Test
     public void onBouncerVisibilityChanged_withoutSidedSecurity_sideFpsHintHidden() {
         setupConditionsToEnableSideFpsHint();
-        setSidedSecurityMode(false);
+        setSideFpsHintEnabledFromResources(false);
         reset(mSidefpsController);
 
         mKeyguardSecurityContainerController.onBouncerVisibilityChanged(View.VISIBLE);
@@ -383,7 +383,7 @@
 
     private void setupConditionsToEnableSideFpsHint() {
         attachView();
-        setSidedSecurityMode(true);
+        setSideFpsHintEnabledFromResources(true);
         setFingerprintDetectionRunning(true);
         setNeedsStrongAuth(false);
     }
@@ -399,8 +399,9 @@
                 BiometricSourceType.FINGERPRINT);
     }
 
-    private void setSidedSecurityMode(boolean sided) {
-        when(mView.isSidedSecurityMode()).thenReturn(sided);
+    private void setSideFpsHintEnabledFromResources(boolean enabled) {
+        when(mResources.getBoolean(R.bool.config_show_sidefps_hint_on_bouncer)).thenReturn(
+                enabled);
     }
 
     private void setNeedsStrongAuth(boolean needed) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 84903d1..85ecfd3 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -86,6 +86,7 @@
 import com.android.internal.widget.ILockSettings;
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardUpdateMonitor.BiometricAuthenticated;
+import com.android.keyguard.logging.KeyguardUpdateMonitorLogger;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.biometrics.AuthController;
 import com.android.systemui.broadcast.BroadcastDispatcher;
@@ -179,6 +180,8 @@
     private KeyguardUpdateMonitorCallback mTestCallback;
     @Mock
     private ActiveUnlockConfig mActiveUnlockConfig;
+    @Mock
+    private KeyguardUpdateMonitorLogger mKeyguardUpdateMonitorLogger;
     // Direct executor
     private Executor mBackgroundExecutor = Runnable::run;
     private Executor mMainExecutor = Runnable::run;
@@ -1189,7 +1192,8 @@
                     mBackgroundExecutor, mMainExecutor,
                     mStatusBarStateController, mLockPatternUtils,
                     mAuthController, mTelephonyListenerManager,
-                    mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig);
+                    mInteractionJankMonitor, mLatencyTracker, mActiveUnlockConfig,
+                    mKeyguardUpdateMonitorLogger);
             setStrongAuthTracker(KeyguardUpdateMonitorTest.this.mStrongAuthTracker);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
index 6157ccb..8d969d0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/SidefpsControllerTest.kt
@@ -45,6 +45,8 @@
 import android.view.ViewPropertyAnimator
 import android.view.WindowInsets
 import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
+import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
 import android.view.WindowMetrics
 import androidx.test.filters.SmallTest
 import com.airbnb.lottie.LottieAnimationView
@@ -438,6 +440,44 @@
 
         assertThat(fingerprintManager.hasSideFpsSensor()).isFalse()
     }
+
+    @Test
+    fun testLayoutParams_hasNoMoveAnimationWindowFlag() = testWithDisplay(
+        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED
+    ) {
+        sideFpsController.overlayOffsets = sensorLocation
+        sideFpsController.updateOverlayParams(
+            windowManager.defaultDisplay,
+            indicatorBounds
+        )
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+        executor.runAllReady()
+
+        verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+        val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+        assertThat((lpFlags and PRIVATE_FLAG_NO_MOVE_ANIMATION) != 0).isTrue()
+    }
+
+    @Test
+    fun testLayoutParams_hasTrustedOverlayWindowFlag() = testWithDisplay(
+        deviceConfig = DeviceConfig.Y_ALIGNED_UNFOLDED
+    ) {
+        sideFpsController.overlayOffsets = sensorLocation
+        sideFpsController.updateOverlayParams(
+            windowManager.defaultDisplay,
+            indicatorBounds
+        )
+        overlayController.show(SENSOR_ID, REASON_UNKNOWN)
+        executor.runAllReady()
+
+        verify(windowManager).updateViewLayout(any(), overlayViewParamsCaptor.capture())
+
+        val lpFlags = overlayViewParamsCaptor.value.privateFlags
+
+        assertThat((lpFlags and PRIVATE_FLAG_TRUSTED_OVERLAY) != 0).isTrue()
+    }
 }
 
 private fun insetsForSmallNavbar() = insetsWithBottom(60)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
index 365c529..2915f5a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationUtilsTest.java
@@ -19,6 +19,7 @@
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_AIR_QUALITY;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_CAST_INFO;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_DATE;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_TIME;
 import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_WEATHER;
 import static com.android.systemui.dreams.complication.ComplicationUtils.convertComplicationType;
@@ -57,6 +58,8 @@
                 .isEqualTo(COMPLICATION_TYPE_AIR_QUALITY);
         assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_CAST_INFO))
                 .isEqualTo(COMPLICATION_TYPE_CAST_INFO);
+        assertThat(convertComplicationType(DreamBackend.COMPLICATION_TYPE_HOME_CONTROLS))
+                .isEqualTo(COMPLICATION_TYPE_HOME_CONTROLS);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockDateComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockDateComplicationTest.java
deleted file mode 100644
index 86aa14d..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamClockDateComplicationTest.java
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * 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.complication;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.content.Context;
-import android.testing.AndroidTestingRunner;
-import android.view.View;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dreams.DreamOverlayStateController;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import javax.inject.Provider;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-public class DreamClockDateComplicationTest extends SysuiTestCase {
-    @SuppressWarnings("HidingField")
-    @Mock
-    private Context mContext;
-
-    @Mock
-    private DreamOverlayStateController mDreamOverlayStateController;
-
-    @Mock
-    private DreamClockDateComplication mComplication;
-
-    @Mock
-    private Provider<DreamClockDateComplication.DreamClockDateViewHolder>
-            mDreamClockDateViewHolderProvider;
-
-    @Mock
-    private DreamClockDateComplication.DreamClockDateViewHolder
-            mDreamClockDateViewHolder;
-
-    @Mock
-    private ComplicationViewModel mComplicationViewModel;
-
-    @Mock
-    private View mView;
-
-    @Mock
-    private ComplicationLayoutParams mLayoutParams;
-
-    @Before
-    public void setup() {
-        MockitoAnnotations.initMocks(this);
-        when(mDreamClockDateViewHolderProvider.get()).thenReturn(mDreamClockDateViewHolder);
-
-    }
-
-    /**
-     * Ensures {@link DreamClockDateComplication} is registered.
-     */
-    @Test
-    public void testComplicationAdded() {
-        final DreamClockDateComplication.Registrant registrant =
-                new DreamClockDateComplication.Registrant(
-                        mContext,
-                        mDreamOverlayStateController,
-                        mComplication);
-        registrant.start();
-        verify(mDreamOverlayStateController).addComplication(eq(mComplication));
-    }
-
-    /**
-     * Verifies {@link DreamClockDateComplication} has the required type.
-     */
-    @Test
-    public void testComplicationRequiredTypeAvailability() {
-        final DreamClockDateComplication complication =
-                new DreamClockDateComplication(mDreamClockDateViewHolderProvider);
-        assertEquals(Complication.COMPLICATION_TYPE_DATE,
-                complication.getRequiredTypeAvailability());
-    }
-
-    /**
-     * Verifies {@link DreamClockDateComplication.DreamClockDateViewHolder} is obtainable from its
-     * provider when the complication creates view.
-     */
-    @Test
-    public void testComplicationViewHolderProviderOnCreateView() {
-        final DreamClockDateComplication complication =
-                new DreamClockDateComplication(mDreamClockDateViewHolderProvider);
-        final Complication.ViewHolder viewHolder = complication.createView(mComplicationViewModel);
-        verify(mDreamClockDateViewHolderProvider).get();
-        assertThat(viewHolder).isEqualTo(mDreamClockDateViewHolder);
-    }
-
-    /**
-     * Verifies {@link DreamClockDateComplication.DreamClockDateViewHolder} has the intended view
-     * and layout parameters from constructor.
-     */
-    @Test
-    public void testComplicationViewHolderContentAccessors() {
-        final DreamClockDateComplication.DreamClockDateViewHolder viewHolder =
-                new DreamClockDateComplication.DreamClockDateViewHolder(mView, mLayoutParams);
-        assertThat(viewHolder.getView()).isEqualTo(mView);
-        assertThat(viewHolder.getLayoutParams()).isEqualTo(mLayoutParams);
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
index 5191f63..04ff7ae 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamHomeControlsComplicationTest.java
@@ -17,6 +17,9 @@
 package com.android.systemui.dreams.complication;
 
 import static com.android.systemui.controls.dagger.ControlsComponent.Visibility.AVAILABLE;
+import static com.android.systemui.dreams.complication.Complication.COMPLICATION_TYPE_HOME_CONTROLS;
+
+import static com.google.common.truth.Truth.assertThat;
 
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
@@ -36,6 +39,7 @@
 import com.android.systemui.controls.dagger.ControlsComponent;
 import com.android.systemui.controls.management.ControlsListingController;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.dreams.complication.dagger.DreamHomeControlsComplicationComponent;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -69,6 +73,9 @@
     @Mock
     private ControlsListingController mControlsListingController;
 
+    @Mock
+    private DreamHomeControlsComplicationComponent.Factory mComponentFactory;
+
     @Captor
     private ArgumentCaptor<ControlsListingController.ControlsListingCallback> mCallbackCaptor;
 
@@ -85,6 +92,14 @@
     }
 
     @Test
+    public void complicationType() {
+        final DreamHomeControlsComplication complication =
+                new DreamHomeControlsComplication(mComponentFactory);
+        assertThat(complication.getRequiredTypeAvailability()).isEqualTo(
+                COMPLICATION_TYPE_HOME_CONTROLS);
+    }
+
+    @Test
     public void complicationAvailability_serviceNotAvailable_noFavorites_doNotAddComplication() {
         final DreamHomeControlsComplication.Registrant registrant =
                 new DreamHomeControlsComplication.Registrant(mContext, mComplication,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
similarity index 94%
rename from packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
index 964e6d7..7d54758 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/SmartSpaceComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/SmartSpaceComplicationTest.java
@@ -13,11 +13,12 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.systemui.dreams;
+package com.android.systemui.dreams.complication;
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -30,8 +31,7 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dreams.complication.Complication;
-import com.android.systemui.dreams.complication.ComplicationViewModel;
+import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.smartspace.DreamSmartspaceController;
 import com.android.systemui.plugins.BcSmartspaceDataPlugin;
 
@@ -183,9 +183,9 @@
 
     @Test
     public void testGetView_reusesSameView() {
-        final SmartSpaceComplication complication = new SmartSpaceComplication(getContext(),
-                mSmartspaceController);
-        final Complication.ViewHolder viewHolder = complication.createView(mComplicationViewModel);
+        final Complication.ViewHolder viewHolder =
+                new SmartSpaceComplication.SmartSpaceComplicationViewHolder(getContext(),
+                        mSmartspaceController, mock(ComplicationLayoutParams.class));
         when(mSmartspaceController.buildAndConnectView(any())).thenReturn(mBcSmartspaceView);
         assertEquals(viewHolder.getView(), viewHolder.getView());
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..592e80b
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
@@ -0,0 +1,99 @@
+/*
+ *  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.keyguard.data.quickaffordance
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
+
+    @Mock private lateinit var component: ControlsComponent
+    @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+
+    private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            HomeControlsKeyguardQuickAffordanceConfig(
+                context = context,
+                component = component,
+            )
+    }
+
+    @Test
+    fun `state - when listing controller is missing - returns None`() = runBlockingTest {
+        whenever(component.isEnabled()).thenReturn(true)
+        whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
+        whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
+        val controlsController = mock<ControlsController>()
+        whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
+        whenever(component.getControlsListingController()).thenReturn(Optional.empty())
+        whenever(controlsController.getFavorites()).thenReturn(listOf(mock()))
+
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.State>()
+        val job = underTest.state.onEach(values::add).launchIn(this)
+
+        assertThat(values.last())
+            .isInstanceOf(KeyguardQuickAffordanceConfig.State.Hidden::class.java)
+        job.cancel()
+    }
+
+    @Test
+    fun `onQuickAffordanceClicked - canShowWhileLockedSetting is true`() = runBlockingTest {
+        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(true))
+
+        val onClickedResult = underTest.onQuickAffordanceClicked(animationController)
+
+        assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
+        assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isTrue()
+    }
+
+    @Test
+    fun `onQuickAffordanceClicked - canShowWhileLockedSetting is false`() = runBlockingTest {
+        whenever(component.canShowWhileLockedSetting).thenReturn(MutableStateFlow(false))
+
+        val onClickedResult = underTest.onQuickAffordanceClicked(animationController)
+
+        assertThat(onClickedResult).isInstanceOf(OnClickedResult.StartActivity::class.java)
+        assertThat((onClickedResult as OnClickedResult.StartActivity).canShowWhileLocked).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..6fd04de
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -0,0 +1,148 @@
+/*
+ *  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.keyguard.data.quickaffordance
+
+import android.content.Intent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
+import com.android.systemui.util.mockito.argumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
+
+    @Mock private lateinit var controller: QRCodeScannerController
+
+    private lateinit var underTest: QrCodeScannerKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(controller.intent).thenReturn(INTENT_1)
+
+        underTest = QrCodeScannerKeyguardQuickAffordanceConfig(controller)
+    }
+
+    @Test
+    fun `affordance - sets up registration and delivers initial model`() = runBlockingTest {
+        whenever(controller.isEnabledForLockScreenButton).thenReturn(true)
+        var latest: KeyguardQuickAffordanceConfig.State? = null
+
+        val job = underTest.state.onEach { latest = it }.launchIn(this)
+
+        val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
+        verify(controller).addCallback(callbackCaptor.capture())
+        verify(controller)
+            .registerQRCodeScannerChangeObservers(
+                QRCodeScannerController.DEFAULT_QR_CODE_SCANNER_CHANGE,
+                QRCodeScannerController.QR_CODE_SCANNER_PREFERENCE_CHANGE
+            )
+        assertVisibleState(latest)
+
+        job.cancel()
+        verify(controller).removeCallback(callbackCaptor.value)
+    }
+
+    @Test
+    fun `affordance - scanner activity changed - delivers model with updated intent`() =
+        runBlockingTest {
+            whenever(controller.isEnabledForLockScreenButton).thenReturn(true)
+            var latest: KeyguardQuickAffordanceConfig.State? = null
+            val job = underTest.state.onEach { latest = it }.launchIn(this)
+            val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
+            verify(controller).addCallback(callbackCaptor.capture())
+
+            whenever(controller.intent).thenReturn(INTENT_2)
+            callbackCaptor.value.onQRCodeScannerActivityChanged()
+
+            assertVisibleState(latest)
+
+            job.cancel()
+            verify(controller).removeCallback(callbackCaptor.value)
+        }
+
+    @Test
+    fun `affordance - scanner preference changed - delivers visible model`() = runBlockingTest {
+        var latest: KeyguardQuickAffordanceConfig.State? = null
+        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
+        verify(controller).addCallback(callbackCaptor.capture())
+
+        whenever(controller.isEnabledForLockScreenButton).thenReturn(true)
+        callbackCaptor.value.onQRCodeScannerPreferenceChanged()
+
+        assertVisibleState(latest)
+
+        job.cancel()
+        verify(controller).removeCallback(callbackCaptor.value)
+    }
+
+    @Test
+    fun `affordance - scanner preference changed - delivers none`() = runBlockingTest {
+        var latest: KeyguardQuickAffordanceConfig.State? = null
+        val job = underTest.state.onEach { latest = it }.launchIn(this)
+        val callbackCaptor = argumentCaptor<QRCodeScannerController.Callback>()
+        verify(controller).addCallback(callbackCaptor.capture())
+
+        whenever(controller.isEnabledForLockScreenButton).thenReturn(false)
+        callbackCaptor.value.onQRCodeScannerPreferenceChanged()
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+
+        job.cancel()
+        verify(controller).removeCallback(callbackCaptor.value)
+    }
+
+    @Test
+    fun onQuickAffordanceClicked() {
+        assertThat(underTest.onQuickAffordanceClicked(mock()))
+            .isEqualTo(
+                OnClickedResult.StartActivity(
+                    intent = INTENT_1,
+                    canShowWhileLocked = true,
+                )
+            )
+    }
+
+    private fun assertVisibleState(latest: KeyguardQuickAffordanceConfig.State?) {
+        assertThat(latest).isInstanceOf(KeyguardQuickAffordanceConfig.State.Visible::class.java)
+        val visibleState = latest as KeyguardQuickAffordanceConfig.State.Visible
+        assertThat(visibleState.icon).isNotNull()
+        assertThat(visibleState.contentDescriptionResourceId).isNotNull()
+    }
+
+    companion object {
+        private val INTENT_1 = Intent("intent1")
+        private val INTENT_2 = Intent("intent2")
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
new file mode 100644
index 0000000..345c51f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -0,0 +1,179 @@
+/*
+ *  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.keyguard.data.quickaffordance
+
+import android.graphics.drawable.Drawable
+import android.service.quickaccesswallet.GetWalletCardsResponse
+import android.service.quickaccesswallet.QuickAccessWalletClient
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.wallet.controller.QuickAccessWalletController
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
+
+    @Mock private lateinit var walletController: QuickAccessWalletController
+    @Mock private lateinit var activityStarter: ActivityStarter
+
+    private lateinit var underTest: QuickAccessWalletKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            QuickAccessWalletKeyguardQuickAffordanceConfig(
+                walletController,
+                activityStarter,
+            )
+    }
+
+    @Test
+    fun `affordance - keyguard showing - has wallet card - visible model`() = runBlockingTest {
+        setUpState()
+        var latest: KeyguardQuickAffordanceConfig.State? = null
+
+        val job = underTest.state.onEach { latest = it }.launchIn(this)
+
+        val visibleModel = latest as KeyguardQuickAffordanceConfig.State.Visible
+        assertThat(visibleModel.icon).isEqualTo(ContainedDrawable.WithDrawable(ICON))
+        assertThat(visibleModel.contentDescriptionResourceId).isNotNull()
+        job.cancel()
+    }
+
+    @Test
+    fun `affordance - wallet not enabled - model is none`() = runBlockingTest {
+        setUpState(isWalletEnabled = false)
+        var latest: KeyguardQuickAffordanceConfig.State? = null
+
+        val job = underTest.state.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+
+        job.cancel()
+    }
+
+    @Test
+    fun `affordance - query not successful - model is none`() = runBlockingTest {
+        setUpState(isWalletQuerySuccessful = false)
+        var latest: KeyguardQuickAffordanceConfig.State? = null
+
+        val job = underTest.state.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+
+        job.cancel()
+    }
+
+    @Test
+    fun `affordance - missing icon - model is none`() = runBlockingTest {
+        setUpState(hasWalletIcon = false)
+        var latest: KeyguardQuickAffordanceConfig.State? = null
+
+        val job = underTest.state.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+
+        job.cancel()
+    }
+
+    @Test
+    fun `affordance - no selected card - model is none`() = runBlockingTest {
+        setUpState(hasWalletIcon = false)
+        var latest: KeyguardQuickAffordanceConfig.State? = null
+
+        val job = underTest.state.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceConfig.State.Hidden)
+
+        job.cancel()
+    }
+
+    @Test
+    fun onQuickAffordanceClicked() {
+        val animationController: ActivityLaunchAnimator.Controller = mock()
+
+        assertThat(underTest.onQuickAffordanceClicked(animationController))
+            .isEqualTo(KeyguardQuickAffordanceConfig.OnClickedResult.Handled)
+        verify(walletController)
+            .startQuickAccessUiIntent(
+                activityStarter,
+                animationController,
+                /* hasCard= */ true,
+            )
+    }
+
+    private fun setUpState(
+        isWalletEnabled: Boolean = true,
+        isWalletQuerySuccessful: Boolean = true,
+        hasWalletIcon: Boolean = true,
+        hasSelectedCard: Boolean = true,
+    ) {
+        whenever(walletController.isWalletEnabled).thenReturn(isWalletEnabled)
+
+        val walletClient: QuickAccessWalletClient = mock()
+        val icon: Drawable? =
+            if (hasWalletIcon) {
+                ICON
+            } else {
+                null
+            }
+        whenever(walletClient.tileIcon).thenReturn(icon)
+        whenever(walletController.walletClient).thenReturn(walletClient)
+
+        whenever(walletController.queryWalletCards(any())).thenAnswer { invocation ->
+            with(
+                invocation.arguments[0] as QuickAccessWalletClient.OnWalletCardsRetrievedCallback
+            ) {
+                if (isWalletQuerySuccessful) {
+                    onWalletCardsRetrieved(
+                        if (hasSelectedCard) {
+                            GetWalletCardsResponse(listOf(mock()), 0)
+                        } else {
+                            GetWalletCardsResponse(emptyList(), 0)
+                        }
+                    )
+                } else {
+                    onWalletCardRetrievalError(mock())
+                }
+            }
+        }
+    }
+
+    companion object {
+        private val ICON: Drawable = mock()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfig.kt
new file mode 100644
index 0000000..6fff440
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfig.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.keyguard.data.repository
+
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnClickedResult
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.yield
+
+/**
+ * Fake implementation of a quick affordance data source.
+ *
+ * This class is abstract to force tests to provide extensions of it as the system that references
+ * these configs uses each implementation's class type to refer to them.
+ */
+abstract class FakeKeyguardQuickAffordanceConfig : KeyguardQuickAffordanceConfig {
+
+    private val _onClickedInvocations = mutableListOf<ActivityLaunchAnimator.Controller?>()
+    val onClickedInvocations: List<ActivityLaunchAnimator.Controller?> = _onClickedInvocations
+
+    var onClickedResult: OnClickedResult = OnClickedResult.Handled
+
+    private val _state =
+        MutableStateFlow<KeyguardQuickAffordanceConfig.State>(
+            KeyguardQuickAffordanceConfig.State.Hidden
+        )
+    override val state: Flow<KeyguardQuickAffordanceConfig.State> = _state
+
+    override fun onQuickAffordanceClicked(
+        animationController: ActivityLaunchAnimator.Controller?,
+    ): OnClickedResult {
+        _onClickedInvocations.add(animationController)
+        return onClickedResult
+    }
+
+    suspend fun setState(state: KeyguardQuickAffordanceConfig.State) {
+        _state.value = state
+        // Yield to allow the test's collection coroutine to "catch up" and collect this value
+        // before the test continues to the next line.
+        // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in
+        // https://developer.android.com/kotlin/flow/test#continuous-collection and remove this.
+        yield()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfigs.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfigs.kt
new file mode 100644
index 0000000..a24fc93
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceConfigs.kt
@@ -0,0 +1,44 @@
+/*
+ * 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.keyguard.data.repository
+
+import com.android.systemui.keyguard.data.config.KeyguardQuickAffordanceConfigs
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
+import kotlin.reflect.KClass
+
+/** Fake implementation of [KeyguardQuickAffordanceConfigs], for tests. */
+class FakeKeyguardQuickAffordanceConfigs(
+    private val configsByPosition:
+        Map<KeyguardQuickAffordancePosition, List<KeyguardQuickAffordanceConfig>>,
+) : KeyguardQuickAffordanceConfigs {
+
+    override fun getAll(
+        position: KeyguardQuickAffordancePosition
+    ): List<KeyguardQuickAffordanceConfig> {
+        return configsByPosition.getValue(position)
+    }
+
+    override fun get(
+        configClass: KClass<out KeyguardQuickAffordanceConfig>
+    ): KeyguardQuickAffordanceConfig {
+        return configsByPosition.values
+            .flatten()
+            .associateBy { config -> config::class }
+            .getValue(configClass)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceRepository.kt
new file mode 100644
index 0000000..10d2e4d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardQuickAffordanceRepository.kt
@@ -0,0 +1,55 @@
+/*
+ * 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.keyguard.data.repository
+
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.yield
+
+/** Fake implementation of [KeyguardQuickAffordanceRepository], for tests. */
+class FakeKeyguardQuickAffordanceRepository : KeyguardQuickAffordanceRepository {
+
+    private val modelByPosition =
+        mutableMapOf<
+            KeyguardQuickAffordancePosition, MutableStateFlow<KeyguardQuickAffordanceModel>>()
+
+    init {
+        KeyguardQuickAffordancePosition.values().forEach { value ->
+            modelByPosition[value] = MutableStateFlow(KeyguardQuickAffordanceModel.Hidden)
+        }
+    }
+
+    override fun affordance(
+        position: KeyguardQuickAffordancePosition
+    ): Flow<KeyguardQuickAffordanceModel> {
+        return modelByPosition.getValue(position)
+    }
+
+    suspend fun setModel(
+        position: KeyguardQuickAffordancePosition,
+        model: KeyguardQuickAffordanceModel
+    ) {
+        modelByPosition.getValue(position).value = model
+        // Yield to allow the test's collection coroutine to "catch up" and collect this value
+        // before the test continues to the next line.
+        // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in
+        // https://developer.android.com/kotlin/flow/test#continuous-collection and remove this.
+        yield()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
new file mode 100644
index 0000000..d40b985
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -0,0 +1,74 @@
+/*
+ * 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.keyguard.data.repository
+
+import com.android.systemui.common.data.model.Position
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+/** Fake implementation of [KeyguardRepository] */
+class FakeKeyguardRepository : KeyguardRepository {
+
+    private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
+    override val animateBottomAreaDozingTransitions: StateFlow<Boolean> =
+        _animateBottomAreaDozingTransitions
+
+    private val _bottomAreaAlpha = MutableStateFlow(1f)
+    override val bottomAreaAlpha: StateFlow<Float> = _bottomAreaAlpha
+
+    private val _clockPosition = MutableStateFlow(Position(0, 0))
+    override val clockPosition: StateFlow<Position> = _clockPosition
+
+    private val _isKeyguardShowing = MutableStateFlow(false)
+    override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing
+
+    private val _isDozing = MutableStateFlow(false)
+    override val isDozing: Flow<Boolean> = _isDozing
+
+    private val _dozeAmount = MutableStateFlow(0f)
+    override val dozeAmount: Flow<Float> = _dozeAmount
+
+    init {
+        setDozeAmount(0f)
+        setDozing(false)
+    }
+
+    override fun setAnimateDozingTransitions(animate: Boolean) {
+        _animateBottomAreaDozingTransitions.tryEmit(animate)
+    }
+
+    override fun setBottomAreaAlpha(alpha: Float) {
+        _bottomAreaAlpha.value = alpha
+    }
+
+    override fun setClockPosition(x: Int, y: Int) {
+        _clockPosition.value = Position(x, y)
+    }
+
+    fun setKeyguardShowing(isShowing: Boolean) {
+        _isKeyguardShowing.value = isShowing
+    }
+
+    fun setDozing(isDozing: Boolean) {
+        _isDozing.value = isDozing
+    }
+
+    fun setDozeAmount(dozeAmount: Float) {
+        _dozeAmount.value = dozeAmount
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
new file mode 100644
index 0000000..bcc76ab
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.keyguard.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.R
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.controls.controller.ControlsController
+import com.android.systemui.controls.dagger.ControlsComponent
+import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameter
+import org.junit.runners.Parameterized.Parameters
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(Parameterized::class)
+class HomeControlsKeyguardQuickAffordanceConfigParameterizedStateTest : SysuiTestCase() {
+
+    companion object {
+        @Parameters(
+            name =
+                "feature enabled = {0}, has favorites = {1}, has service infos = {2} - expected" +
+                    " visible = {3}"
+        )
+        @JvmStatic
+        fun data() =
+            (0 until 8)
+                .map { combination ->
+                    arrayOf(
+                        /* isFeatureEnabled= */ combination and 0b100 != 0,
+                        /* hasFavorites= */ combination and 0b010 != 0,
+                        /* hasServiceInfos= */ combination and 0b001 != 0,
+                        /* isVisible= */ combination == 0b111,
+                    )
+                }
+                .toList()
+    }
+
+    @Mock private lateinit var component: ControlsComponent
+    @Mock private lateinit var controlsController: ControlsController
+    @Mock private lateinit var controlsListingController: ControlsListingController
+    @Captor
+    private lateinit var callbackCaptor:
+        ArgumentCaptor<ControlsListingController.ControlsListingCallback>
+
+    private lateinit var underTest: HomeControlsKeyguardQuickAffordanceConfig
+
+    @JvmField @Parameter(0) var isFeatureEnabled: Boolean = false
+    @JvmField @Parameter(1) var hasFavorites: Boolean = false
+    @JvmField @Parameter(2) var hasServiceInfos: Boolean = false
+    @JvmField @Parameter(3) var isVisible: Boolean = false
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(component.getTileImageId()).thenReturn(R.drawable.controls_icon)
+        whenever(component.getTileTitleId()).thenReturn(R.string.quick_controls_title)
+        whenever(component.getControlsController()).thenReturn(Optional.of(controlsController))
+        whenever(component.getControlsListingController())
+            .thenReturn(Optional.of(controlsListingController))
+
+        underTest =
+            HomeControlsKeyguardQuickAffordanceConfig(
+                context = context,
+                component = component,
+            )
+    }
+
+    @Test
+    fun state() = runBlockingTest {
+        whenever(component.isEnabled()).thenReturn(isFeatureEnabled)
+        whenever(controlsController.getFavorites())
+            .thenReturn(
+                if (hasFavorites) {
+                    listOf(mock())
+                } else {
+                    emptyList()
+                }
+            )
+        val values = mutableListOf<KeyguardQuickAffordanceConfig.State>()
+        val job = underTest.state.onEach(values::add).launchIn(this)
+
+        verify(controlsListingController).addCallback(callbackCaptor.capture())
+        callbackCaptor.value.onServicesUpdated(
+            if (hasServiceInfos) {
+                listOf(mock())
+            } else {
+                emptyList()
+            }
+        )
+
+        assertThat(values.last())
+            .isInstanceOf(
+                if (isVisible) {
+                    KeyguardQuickAffordanceConfig.State.Visible::class.java
+                } else {
+                    KeyguardQuickAffordanceConfig.State.Hidden::class.java
+                }
+            )
+        job.cancel()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryImplTest.kt
new file mode 100644
index 0000000..dc0e6f7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryImplTest.kt
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.reflect.KClass
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordanceRepositoryImplTest : SysuiTestCase() {
+
+    private lateinit var underTest: KeyguardQuickAffordanceRepository
+
+    private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig
+    private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig
+    private lateinit var qrCodeScanner: FakeKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        homeControls = object : FakeKeyguardQuickAffordanceConfig() {}
+        quickAccessWallet = object : FakeKeyguardQuickAffordanceConfig() {}
+        qrCodeScanner = object : FakeKeyguardQuickAffordanceConfig() {}
+
+        underTest =
+            KeyguardQuickAffordanceRepositoryImpl(
+                configs =
+                    FakeKeyguardQuickAffordanceConfigs(
+                        mapOf(
+                            KeyguardQuickAffordancePosition.BOTTOM_START to
+                                listOf(
+                                    homeControls,
+                                ),
+                            KeyguardQuickAffordancePosition.BOTTOM_END to
+                                listOf(
+                                    quickAccessWallet,
+                                    qrCodeScanner,
+                                ),
+                        ),
+                    ),
+            )
+    }
+
+    @Test
+    fun `bottom start affordance - none`() = runBlockingTest {
+        // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in
+        // https://developer.android.com/kotlin/flow/test#continuous-collection
+        var latest: KeyguardQuickAffordanceModel? = null
+        val job =
+            underTest
+                .affordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
+        job.cancel()
+    }
+
+    @Test
+    fun `bottom start affordance - home controls`() = runBlockingTest {
+        // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in
+        // https://developer.android.com/kotlin/flow/test#continuous-collection
+        var latest: KeyguardQuickAffordanceModel? = null
+        val job =
+            underTest
+                .affordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                .onEach { latest = it }
+                .launchIn(this)
+
+        val state =
+            KeyguardQuickAffordanceConfig.State.Visible(
+                icon = mock(),
+                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
+            )
+        homeControls.setState(state)
+
+        assertThat(latest).isEqualTo(state.toModel(homeControls::class))
+        job.cancel()
+    }
+
+    @Test
+    fun `bottom end affordance - none`() = runBlockingTest {
+        // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in
+        // https://developer.android.com/kotlin/flow/test#continuous-collection
+        var latest: KeyguardQuickAffordanceModel? = null
+        val job =
+            underTest
+                .affordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
+        job.cancel()
+    }
+
+    @Test
+    fun `bottom end affordance - quick access wallet`() = runBlockingTest {
+        // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in
+        // https://developer.android.com/kotlin/flow/test#continuous-collection
+        var latest: KeyguardQuickAffordanceModel? = null
+        val job =
+            underTest
+                .affordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                .onEach { latest = it }
+                .launchIn(this)
+
+        val quickAccessWalletState =
+            KeyguardQuickAffordanceConfig.State.Visible(
+                icon = mock(),
+                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
+            )
+        quickAccessWallet.setState(quickAccessWalletState)
+        val qrCodeScannerState =
+            KeyguardQuickAffordanceConfig.State.Visible(
+                icon = mock(),
+                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
+            )
+        qrCodeScanner.setState(qrCodeScannerState)
+
+        assertThat(latest).isEqualTo(quickAccessWalletState.toModel(quickAccessWallet::class))
+        job.cancel()
+    }
+
+    @Test
+    fun `bottom end affordance - qr code scanner`() = runBlockingTest {
+        // TODO(b/239834928): once coroutines.test is updated, switch to the approach described in
+        // https://developer.android.com/kotlin/flow/test#continuous-collection
+        var latest: KeyguardQuickAffordanceModel? = null
+        val job =
+            underTest
+                .affordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                .onEach { latest = it }
+                .launchIn(this)
+
+        val state =
+            KeyguardQuickAffordanceConfig.State.Visible(
+                icon = mock(),
+                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
+            )
+        qrCodeScanner.setState(state)
+
+        assertThat(latest).isEqualTo(state.toModel(qrCodeScanner::class))
+        job.cancel()
+    }
+
+    private fun KeyguardQuickAffordanceConfig.State?.toModel(
+        configKey: KClass<out KeyguardQuickAffordanceConfig>,
+    ): KeyguardQuickAffordanceModel? {
+        return when (this) {
+            is KeyguardQuickAffordanceConfig.State.Visible ->
+                KeyguardQuickAffordanceModel.Visible(
+                    configKey = configKey,
+                    icon = icon,
+                    contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
+                )
+            is KeyguardQuickAffordanceConfig.State.Hidden -> KeyguardQuickAffordanceModel.Hidden
+            null -> null
+        }
+    }
+
+    companion object {
+        private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
new file mode 100644
index 0000000..3d2c51a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -0,0 +1,162 @@
+/*
+ * 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.keyguard.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.data.model.Position
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.mockito.argumentCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardRepositoryImplTest : SysuiTestCase() {
+
+    @Mock private lateinit var statusBarStateController: StatusBarStateController
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+
+    private lateinit var underTest: KeyguardRepositoryImpl
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest = KeyguardRepositoryImpl(statusBarStateController, keyguardStateController)
+    }
+
+    @Test
+    fun animateBottomAreaDozingTransitions() = runBlockingTest {
+        assertThat(underTest.animateBottomAreaDozingTransitions.value).isEqualTo(false)
+
+        underTest.setAnimateDozingTransitions(true)
+        assertThat(underTest.animateBottomAreaDozingTransitions.value).isTrue()
+
+        underTest.setAnimateDozingTransitions(false)
+        assertThat(underTest.animateBottomAreaDozingTransitions.value).isFalse()
+
+        underTest.setAnimateDozingTransitions(true)
+        assertThat(underTest.animateBottomAreaDozingTransitions.value).isTrue()
+    }
+
+    @Test
+    fun bottomAreaAlpha() = runBlockingTest {
+        assertThat(underTest.bottomAreaAlpha.value).isEqualTo(1f)
+
+        underTest.setBottomAreaAlpha(0.1f)
+        assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.1f)
+
+        underTest.setBottomAreaAlpha(0.2f)
+        assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.2f)
+
+        underTest.setBottomAreaAlpha(0.3f)
+        assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.3f)
+
+        underTest.setBottomAreaAlpha(0.5f)
+        assertThat(underTest.bottomAreaAlpha.value).isEqualTo(0.5f)
+
+        underTest.setBottomAreaAlpha(1.0f)
+        assertThat(underTest.bottomAreaAlpha.value).isEqualTo(1f)
+    }
+
+    @Test
+    fun clockPosition() = runBlockingTest {
+        assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 0))
+
+        underTest.setClockPosition(0, 1)
+        assertThat(underTest.clockPosition.value).isEqualTo(Position(0, 1))
+
+        underTest.setClockPosition(1, 9)
+        assertThat(underTest.clockPosition.value).isEqualTo(Position(1, 9))
+
+        underTest.setClockPosition(1, 0)
+        assertThat(underTest.clockPosition.value).isEqualTo(Position(1, 0))
+
+        underTest.setClockPosition(3, 1)
+        assertThat(underTest.clockPosition.value).isEqualTo(Position(3, 1))
+    }
+
+    @Test
+    fun isKeyguardShowing() = runBlockingTest {
+        whenever(keyguardStateController.isShowing).thenReturn(false)
+        var latest: Boolean? = null
+        val job = underTest.isKeyguardShowing.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        val captor = argumentCaptor<KeyguardStateController.Callback>()
+        verify(keyguardStateController).addCallback(captor.capture())
+
+        whenever(keyguardStateController.isShowing).thenReturn(true)
+        captor.value.onKeyguardShowingChanged()
+        assertThat(latest).isTrue()
+
+        whenever(keyguardStateController.isShowing).thenReturn(false)
+        captor.value.onKeyguardShowingChanged()
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun isDozing() = runBlockingTest {
+        var latest: Boolean? = null
+        val job = underTest.isDozing.onEach { latest = it }.launchIn(this)
+
+        val captor = argumentCaptor<StatusBarStateController.StateListener>()
+        verify(statusBarStateController).addCallback(captor.capture())
+
+        captor.value.onDozingChanged(true)
+        assertThat(latest).isTrue()
+
+        captor.value.onDozingChanged(false)
+        assertThat(latest).isFalse()
+
+        job.cancel()
+        verify(statusBarStateController).removeCallback(captor.value)
+    }
+
+    @Test
+    fun dozeAmount() = runBlockingTest {
+        val values = mutableListOf<Float>()
+        val job = underTest.dozeAmount.onEach(values::add).launchIn(this)
+
+        val captor = argumentCaptor<StatusBarStateController.StateListener>()
+        verify(statusBarStateController).addCallback(captor.capture())
+
+        captor.value.onDozeAmountChanged(0.433f, 0.4f)
+        captor.value.onDozeAmountChanged(0.498f, 0.5f)
+        captor.value.onDozeAmountChanged(0.661f, 0.65f)
+
+        assertThat(values).isEqualTo(listOf(0f, 0.4f, 0.5f, 0.65f))
+
+        job.cancel()
+        verify(statusBarStateController).removeCallback(captor.value)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/FakeLaunchKeyguardQuickAffordanceUseCase.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/FakeLaunchKeyguardQuickAffordanceUseCase.kt
new file mode 100644
index 0000000..ba0c31f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/FakeLaunchKeyguardQuickAffordanceUseCase.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import android.content.Intent
+import com.android.systemui.animation.ActivityLaunchAnimator
+
+/** Fake implementation of [LaunchKeyguardQuickAffordanceUseCase], for tests. */
+class FakeLaunchKeyguardQuickAffordanceUseCase : LaunchKeyguardQuickAffordanceUseCase {
+
+    data class Invocation(
+        val intent: Intent,
+        val canShowWhileLocked: Boolean,
+        val animationController: ActivityLaunchAnimator.Controller?
+    )
+
+    private val _invocations = mutableListOf<Invocation>()
+    val invocations: List<Invocation> = _invocations
+
+    override fun invoke(
+        intent: Intent,
+        canShowWhileLocked: Boolean,
+        animationController: ActivityLaunchAnimator.Controller?
+    ) {
+        _invocations.add(
+            Invocation(
+                intent = intent,
+                canShowWhileLocked = canShowWhileLocked,
+                animationController = animationController,
+            )
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/LaunchKeyguardQuickAffordanceUseCaseImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/LaunchKeyguardQuickAffordanceUseCaseImplTest.kt
new file mode 100644
index 0000000..b3c1ae0
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/LaunchKeyguardQuickAffordanceUseCaseImplTest.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import android.content.Intent
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.junit.runners.Parameterized.Parameter
+import org.junit.runners.Parameterized.Parameters
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(Parameterized::class)
+class LaunchKeyguardQuickAffordanceUseCaseImplTest : SysuiTestCase() {
+
+    companion object {
+        private val INTENT = Intent("some.intent.action")
+
+        @Parameters(
+            name =
+                "needStrongAuthAfterBoot={0}, canShowWhileLocked={1}," +
+                    " keyguardIsUnlocked={2}, needsToUnlockFirst={3}"
+        )
+        @JvmStatic
+        fun data() =
+            listOf(
+                arrayOf(
+                    /* needStrongAuthAfterBoot= */ false,
+                    /* canShowWhileLocked= */ false,
+                    /* keyguardIsUnlocked= */ false,
+                    /* needsToUnlockFirst= */ true,
+                ),
+                arrayOf(
+                    /* needStrongAuthAfterBoot= */ false,
+                    /* canShowWhileLocked= */ false,
+                    /* keyguardIsUnlocked= */ true,
+                    /* needsToUnlockFirst= */ false,
+                ),
+                arrayOf(
+                    /* needStrongAuthAfterBoot= */ false,
+                    /* canShowWhileLocked= */ true,
+                    /* keyguardIsUnlocked= */ false,
+                    /* needsToUnlockFirst= */ false,
+                ),
+                arrayOf(
+                    /* needStrongAuthAfterBoot= */ false,
+                    /* canShowWhileLocked= */ true,
+                    /* keyguardIsUnlocked= */ true,
+                    /* needsToUnlockFirst= */ false,
+                ),
+                arrayOf(
+                    /* needStrongAuthAfterBoot= */ true,
+                    /* canShowWhileLocked= */ false,
+                    /* keyguardIsUnlocked= */ false,
+                    /* needsToUnlockFirst= */ true,
+                ),
+                arrayOf(
+                    /* needStrongAuthAfterBoot= */ true,
+                    /* canShowWhileLocked= */ false,
+                    /* keyguardIsUnlocked= */ true,
+                    /* needsToUnlockFirst= */ true,
+                ),
+                arrayOf(
+                    /* needStrongAuthAfterBoot= */ true,
+                    /* canShowWhileLocked= */ true,
+                    /* keyguardIsUnlocked= */ false,
+                    /* needsToUnlockFirst= */ true,
+                ),
+                arrayOf(
+                    /* needStrongAuthAfterBoot= */ true,
+                    /* canShowWhileLocked= */ true,
+                    /* keyguardIsUnlocked= */ true,
+                    /* needsToUnlockFirst= */ true,
+                ),
+            )
+    }
+
+    @Mock private lateinit var lockPatternUtils: LockPatternUtils
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var userTracker: UserTracker
+    @Mock private lateinit var activityStarter: ActivityStarter
+    @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+
+    private lateinit var underTest: LaunchKeyguardQuickAffordanceUseCase
+
+    @JvmField @Parameter(0) var needStrongAuthAfterBoot: Boolean = false
+    @JvmField @Parameter(1) var canShowWhileLocked: Boolean = false
+    @JvmField @Parameter(2) var keyguardIsUnlocked: Boolean = false
+    @JvmField @Parameter(3) var needsToUnlockFirst: Boolean = false
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        underTest =
+            LaunchKeyguardQuickAffordanceUseCaseImpl(
+                lockPatternUtils = lockPatternUtils,
+                keyguardStateController = keyguardStateController,
+                userTracker = userTracker,
+                activityStarter = activityStarter,
+            )
+    }
+
+    @Test
+    fun invoke() {
+        setUpMocks(
+            needStrongAuthAfterBoot = needStrongAuthAfterBoot,
+            keyguardIsUnlocked = keyguardIsUnlocked,
+        )
+
+        underTest(
+            intent = INTENT,
+            canShowWhileLocked = canShowWhileLocked,
+            animationController = animationController,
+        )
+
+        if (needsToUnlockFirst) {
+            verify(activityStarter)
+                .postStartActivityDismissingKeyguard(
+                    INTENT,
+                    /* delay= */ 0,
+                    animationController,
+                )
+        } else {
+            verify(activityStarter)
+                .startActivity(
+                    INTENT,
+                    /* dismissShade= */ true,
+                    animationController,
+                    /* showOverLockscreenWhenLocked= */ true,
+                )
+        }
+    }
+
+    private fun setUpMocks(
+        needStrongAuthAfterBoot: Boolean = true,
+        keyguardIsUnlocked: Boolean = false,
+    ) {
+        whenever(userTracker.userHandle).thenReturn(mock())
+        whenever(lockPatternUtils.getStrongAuthForUser(any()))
+            .thenReturn(
+                if (needStrongAuthAfterBoot) {
+                    LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
+                } else {
+                    LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED
+                }
+            )
+        whenever(keyguardStateController.isUnlocked).thenReturn(keyguardIsUnlocked)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/ObserveKeyguardQuickAffordanceUseCaseTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/ObserveKeyguardQuickAffordanceUseCaseTest.kt
new file mode 100644
index 0000000..b90400be
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/usecase/ObserveKeyguardQuickAffordanceUseCaseTest.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.keyguard.domain.usecase
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.keyguard.data.quickaffordance.HomeControlsKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.repository.FakeKeyguardQuickAffordanceRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class ObserveKeyguardQuickAffordanceUseCaseTest : SysuiTestCase() {
+
+    private lateinit var underTest: ObserveKeyguardQuickAffordanceUseCase
+
+    private lateinit var repository: FakeKeyguardRepository
+    private lateinit var quickAffordanceRepository: FakeKeyguardQuickAffordanceRepository
+    private lateinit var isDozingUseCase: ObserveIsDozingUseCase
+    private lateinit var isKeyguardShowingUseCase: ObserveIsKeyguardShowingUseCase
+
+    @Before
+    fun setUp() {
+        repository = FakeKeyguardRepository()
+        repository.setKeyguardShowing(true)
+        isDozingUseCase = ObserveIsDozingUseCase(repository)
+        isKeyguardShowingUseCase = ObserveIsKeyguardShowingUseCase(repository)
+        quickAffordanceRepository = FakeKeyguardQuickAffordanceRepository()
+
+        underTest =
+            ObserveKeyguardQuickAffordanceUseCase(
+                repository = quickAffordanceRepository,
+                isDozingUseCase = isDozingUseCase,
+                isKeyguardShowingUseCase = isKeyguardShowingUseCase,
+            )
+    }
+
+    @Test
+    fun `invoke - affordance is visible`() = runBlockingTest {
+        val configKey = HomeControlsKeyguardQuickAffordanceConfig::class
+        val model =
+            KeyguardQuickAffordanceModel.Visible(
+                configKey = configKey,
+                icon = ICON,
+                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
+            )
+        quickAffordanceRepository.setModel(
+            KeyguardQuickAffordancePosition.BOTTOM_END,
+            model,
+        )
+
+        var latest: KeyguardQuickAffordanceModel? = null
+        val job =
+            underTest(KeyguardQuickAffordancePosition.BOTTOM_END)
+                .onEach { latest = it }
+                .launchIn(this)
+
+        assertThat(latest).isInstanceOf(KeyguardQuickAffordanceModel.Visible::class.java)
+        val visibleModel = latest as KeyguardQuickAffordanceModel.Visible
+        assertThat(visibleModel.configKey).isEqualTo(configKey)
+        assertThat(visibleModel.icon).isEqualTo(ICON)
+        assertThat(visibleModel.contentDescriptionResourceId)
+            .isEqualTo(CONTENT_DESCRIPTION_RESOURCE_ID)
+        job.cancel()
+    }
+
+    @Test
+    fun `invoke - affordance not visible while dozing`() = runBlockingTest {
+        repository.setDozing(true)
+        val configKey = HomeControlsKeyguardQuickAffordanceConfig::class
+        val model =
+            KeyguardQuickAffordanceModel.Visible(
+                configKey = configKey,
+                icon = ICON,
+                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
+            )
+        quickAffordanceRepository.setModel(
+            KeyguardQuickAffordancePosition.BOTTOM_END,
+            model,
+        )
+
+        var latest: KeyguardQuickAffordanceModel? = null
+        val job =
+            underTest(KeyguardQuickAffordancePosition.BOTTOM_END)
+                .onEach { latest = it }
+                .launchIn(this)
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
+        job.cancel()
+    }
+
+    @Test
+    fun `invoke - affordance not visible when lockscreen is not showing`() = runBlockingTest {
+        repository.setKeyguardShowing(false)
+        val configKey = HomeControlsKeyguardQuickAffordanceConfig::class
+        val model =
+            KeyguardQuickAffordanceModel.Visible(
+                configKey = configKey,
+                icon = ICON,
+                contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
+            )
+        quickAffordanceRepository.setModel(
+            KeyguardQuickAffordancePosition.BOTTOM_END,
+            model,
+        )
+
+        var latest: KeyguardQuickAffordanceModel? = null
+        val job =
+            underTest(KeyguardQuickAffordancePosition.BOTTOM_END)
+                .onEach { latest = it }
+                .launchIn(this)
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
+        job.cancel()
+    }
+
+    @Test
+    fun `invoke - affordance is none`() = runBlockingTest {
+        quickAffordanceRepository.setModel(
+            KeyguardQuickAffordancePosition.BOTTOM_START,
+            KeyguardQuickAffordanceModel.Hidden,
+        )
+
+        var latest: KeyguardQuickAffordanceModel? = null
+        val job =
+            underTest(KeyguardQuickAffordancePosition.BOTTOM_START)
+                .onEach { latest = it }
+                .launchIn(this)
+        assertThat(latest).isEqualTo(KeyguardQuickAffordanceModel.Hidden)
+        job.cancel()
+    }
+
+    companion object {
+        private val ICON: ContainedDrawable = mock()
+        private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
new file mode 100644
index 0000000..00dd58e
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -0,0 +1,503 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import android.content.Intent
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.containeddrawable.ContainedDrawable
+import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.repository.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.repository.FakeKeyguardQuickAffordanceConfigs
+import com.android.systemui.keyguard.data.repository.FakeKeyguardQuickAffordanceRepository
+import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.domain.usecase.FakeLaunchKeyguardQuickAffordanceUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveAnimateBottomAreaTransitionsUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveBottomAreaAlphaUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveClockPositionUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveDozeAmountUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveIsDozingUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveIsKeyguardShowingUseCase
+import com.android.systemui.keyguard.domain.usecase.ObserveKeyguardQuickAffordanceUseCase
+import com.android.systemui.keyguard.domain.usecase.OnKeyguardQuickAffordanceClickedUseCase
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordanceModel
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePosition
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import kotlin.reflect.KClass
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.runBlockingTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardBottomAreaViewModelTest : SysuiTestCase() {
+
+    @Mock private lateinit var animationController: ActivityLaunchAnimator.Controller
+    @Mock private lateinit var burnInHelperWrapper: BurnInHelperWrapper
+
+    private lateinit var underTest: KeyguardBottomAreaViewModel
+
+    private lateinit var affordanceRepository: FakeKeyguardQuickAffordanceRepository
+    private lateinit var repository: FakeKeyguardRepository
+    private lateinit var isDozingUseCase: ObserveIsDozingUseCase
+    private lateinit var isKeyguardShowingUseCase: ObserveIsKeyguardShowingUseCase
+    private lateinit var launchQuickAffordanceUseCase: FakeLaunchKeyguardQuickAffordanceUseCase
+    private lateinit var homeControlsQuickAffordanceConfig: FakeKeyguardQuickAffordanceConfig
+    private lateinit var quickAccessWalletAffordanceConfig: FakeKeyguardQuickAffordanceConfig
+    private lateinit var qrCodeScannerAffordanceConfig: FakeKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        whenever(burnInHelperWrapper.burnInOffset(anyInt(), any()))
+            .thenReturn(RETURNED_BURN_IN_OFFSET)
+
+        affordanceRepository = FakeKeyguardQuickAffordanceRepository()
+        repository = FakeKeyguardRepository()
+        isDozingUseCase =
+            ObserveIsDozingUseCase(
+                repository = repository,
+            )
+        isKeyguardShowingUseCase =
+            ObserveIsKeyguardShowingUseCase(
+                repository = repository,
+            )
+        launchQuickAffordanceUseCase = FakeLaunchKeyguardQuickAffordanceUseCase()
+        homeControlsQuickAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
+        quickAccessWalletAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
+        qrCodeScannerAffordanceConfig = object : FakeKeyguardQuickAffordanceConfig() {}
+
+        underTest =
+            KeyguardBottomAreaViewModel(
+                observeQuickAffordanceUseCase =
+                    ObserveKeyguardQuickAffordanceUseCase(
+                        repository = affordanceRepository,
+                        isDozingUseCase = isDozingUseCase,
+                        isKeyguardShowingUseCase = isKeyguardShowingUseCase,
+                    ),
+                onQuickAffordanceClickedUseCase =
+                    OnKeyguardQuickAffordanceClickedUseCase(
+                        configs =
+                            FakeKeyguardQuickAffordanceConfigs(
+                                mapOf(
+                                    KeyguardQuickAffordancePosition.BOTTOM_START to
+                                        listOf(
+                                            homeControlsQuickAffordanceConfig,
+                                        ),
+                                    KeyguardQuickAffordancePosition.BOTTOM_END to
+                                        listOf(
+                                            quickAccessWalletAffordanceConfig,
+                                            qrCodeScannerAffordanceConfig,
+                                        ),
+                                ),
+                            ),
+                        launchAffordanceUseCase = launchQuickAffordanceUseCase,
+                    ),
+                observeBottomAreaAlphaUseCase =
+                    ObserveBottomAreaAlphaUseCase(
+                        repository = repository,
+                    ),
+                observeIsDozingUseCase = isDozingUseCase,
+                observeAnimateBottomAreaTransitionsUseCase =
+                    ObserveAnimateBottomAreaTransitionsUseCase(
+                        repository = repository,
+                    ),
+                observeDozeAmountUseCase =
+                    ObserveDozeAmountUseCase(
+                        repository = repository,
+                    ),
+                observeClockPositionUseCase =
+                    ObserveClockPositionUseCase(
+                        repository = repository,
+                    ),
+                burnInHelperWrapper = burnInHelperWrapper,
+            )
+    }
+
+    @Test
+    fun `startButton - present - not dozing - lockscreen showing - visible model - starts activity on click`() = // ktlint-disable max-line-length
+        runBlockingTest {
+            var latest: KeyguardQuickAffordanceViewModel? = null
+            val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+
+            repository.setDozing(false)
+            repository.setKeyguardShowing(true)
+            val testConfig =
+                TestConfig(
+                    isVisible = true,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                    intent = Intent("action"),
+                )
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = testConfig,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest,
+                testConfig = testConfig,
+                configKey = configKey,
+            )
+            job.cancel()
+        }
+
+    @Test
+    fun `endButton - present - not dozing - lockscreen showing - visible model - do nothing on click`() = // ktlint-disable max-line-length
+        runBlockingTest {
+            var latest: KeyguardQuickAffordanceViewModel? = null
+            val job = underTest.endButton.onEach { latest = it }.launchIn(this)
+
+            repository.setDozing(false)
+            repository.setKeyguardShowing(true)
+            val config =
+                TestConfig(
+                    isVisible = true,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                    intent =
+                        null, // This will cause it to tell the system that the click was handled.
+                )
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_END,
+                    testConfig = config,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest,
+                testConfig = config,
+                configKey = configKey,
+            )
+            job.cancel()
+        }
+
+    @Test
+    fun `startButton - not present - not dozing - lockscreen showing - model is none`() =
+        runBlockingTest {
+            var latest: KeyguardQuickAffordanceViewModel? = null
+            val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+
+            repository.setDozing(false)
+            repository.setKeyguardShowing(true)
+            val config =
+                TestConfig(
+                    isVisible = false,
+                )
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = config,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest,
+                testConfig = config,
+                configKey = configKey,
+            )
+            job.cancel()
+        }
+
+    @Test
+    fun `startButton - present - dozing - lockscreen showing - model is none`() = runBlockingTest {
+        var latest: KeyguardQuickAffordanceViewModel? = null
+        val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+
+        repository.setDozing(true)
+        repository.setKeyguardShowing(true)
+        val config =
+            TestConfig(
+                isVisible = true,
+                icon = mock(),
+                canShowWhileLocked = false,
+                intent = Intent("action"),
+            )
+        val configKey =
+            setUpQuickAffordanceModel(
+                position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                testConfig = config,
+            )
+
+        assertQuickAffordanceViewModel(
+            viewModel = latest,
+            testConfig = TestConfig(isVisible = false),
+            configKey = configKey,
+        )
+        job.cancel()
+    }
+
+    @Test
+    fun `startButton - present - not dozing - lockscreen not showing - model is none`() =
+        runBlockingTest {
+            var latest: KeyguardQuickAffordanceViewModel? = null
+            val job = underTest.startButton.onEach { latest = it }.launchIn(this)
+
+            repository.setDozing(false)
+            repository.setKeyguardShowing(false)
+            val config =
+                TestConfig(
+                    isVisible = true,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                    intent = Intent("action"),
+                )
+            val configKey =
+                setUpQuickAffordanceModel(
+                    position = KeyguardQuickAffordancePosition.BOTTOM_START,
+                    testConfig = config,
+                )
+
+            assertQuickAffordanceViewModel(
+                viewModel = latest,
+                testConfig = TestConfig(isVisible = false),
+                configKey = configKey,
+            )
+            job.cancel()
+        }
+
+    @Test
+    fun animateButtonReveal() = runBlockingTest {
+        val values = mutableListOf<Boolean>()
+        val job = underTest.animateButtonReveal.onEach(values::add).launchIn(this)
+
+        repository.setAnimateDozingTransitions(true)
+        repository.setAnimateDozingTransitions(false)
+
+        assertThat(values).isEqualTo(listOf(false, true, false))
+        job.cancel()
+    }
+
+    @Test
+    fun isOverlayContainerVisible() = runBlockingTest {
+        val values = mutableListOf<Boolean>()
+        val job = underTest.isOverlayContainerVisible.onEach(values::add).launchIn(this)
+
+        repository.setDozing(true)
+        repository.setDozing(false)
+
+        assertThat(values).isEqualTo(listOf(true, false, true))
+        job.cancel()
+    }
+
+    @Test
+    fun alpha() = runBlockingTest {
+        val values = mutableListOf<Float>()
+        val job = underTest.alpha.onEach(values::add).launchIn(this)
+
+        repository.setBottomAreaAlpha(0.1f)
+        repository.setBottomAreaAlpha(0.5f)
+        repository.setBottomAreaAlpha(0.2f)
+        repository.setBottomAreaAlpha(0f)
+
+        assertThat(values).isEqualTo(listOf(1f, 0.1f, 0.5f, 0.2f, 0f))
+        job.cancel()
+    }
+
+    @Test
+    fun isIndicationAreaPadded() = runBlockingTest {
+        repository.setKeyguardShowing(true)
+        val values = mutableListOf<Boolean>()
+        val job = underTest.isIndicationAreaPadded.onEach(values::add).launchIn(this)
+
+        setUpQuickAffordanceModel(
+            position = KeyguardQuickAffordancePosition.BOTTOM_START,
+            testConfig =
+                TestConfig(
+                    isVisible = true,
+                    icon = mock(),
+                    canShowWhileLocked = true,
+                )
+        )
+        setUpQuickAffordanceModel(
+            position = KeyguardQuickAffordancePosition.BOTTOM_END,
+            testConfig =
+                TestConfig(
+                    isVisible = true,
+                    icon = mock(),
+                    canShowWhileLocked = false,
+                )
+        )
+        setUpQuickAffordanceModel(
+            position = KeyguardQuickAffordancePosition.BOTTOM_START,
+            testConfig =
+                TestConfig(
+                    isVisible = false,
+                )
+        )
+        setUpQuickAffordanceModel(
+            position = KeyguardQuickAffordancePosition.BOTTOM_END,
+            testConfig =
+                TestConfig(
+                    isVisible = false,
+                )
+        )
+
+        assertThat(values)
+            .isEqualTo(
+                listOf(
+                    // Initially, no button is visible so the indication area is not padded.
+                    false,
+                    // Once we add the first visible button, the indication area becomes padded.
+                    // This
+                    // continues to be true after we add the second visible button and even after we
+                    // make the first button not visible anymore.
+                    true,
+                    // Once both buttons are not visible, the indication area is, again, not padded.
+                    false,
+                )
+            )
+        job.cancel()
+    }
+
+    @Test
+    fun indicationAreaTranslationX() = runBlockingTest {
+        val values = mutableListOf<Float>()
+        val job = underTest.indicationAreaTranslationX.onEach(values::add).launchIn(this)
+
+        repository.setClockPosition(100, 100)
+        repository.setClockPosition(200, 100)
+        repository.setClockPosition(200, 200)
+        repository.setClockPosition(300, 100)
+
+        assertThat(values).isEqualTo(listOf(0f, 100f, 200f, 300f))
+        job.cancel()
+    }
+
+    @Test
+    fun indicationAreaTranslationY() = runBlockingTest {
+        val values = mutableListOf<Float>()
+        val job =
+            underTest
+                .indicationAreaTranslationY(DEFAULT_BURN_IN_OFFSET)
+                .onEach(values::add)
+                .launchIn(this)
+
+        val expectedTranslationValues =
+            listOf(
+                -0f, // Negative 0 - apparently there's a difference in floating point arithmetic -
+                // FML
+                setDozeAmountAndCalculateExpectedTranslationY(0.1f),
+                setDozeAmountAndCalculateExpectedTranslationY(0.2f),
+                setDozeAmountAndCalculateExpectedTranslationY(0.5f),
+                setDozeAmountAndCalculateExpectedTranslationY(1f),
+            )
+
+        assertThat(values).isEqualTo(expectedTranslationValues)
+        job.cancel()
+    }
+
+    private fun setDozeAmountAndCalculateExpectedTranslationY(dozeAmount: Float): Float {
+        repository.setDozeAmount(dozeAmount)
+        return dozeAmount * (RETURNED_BURN_IN_OFFSET - DEFAULT_BURN_IN_OFFSET)
+    }
+
+    private suspend fun setUpQuickAffordanceModel(
+        position: KeyguardQuickAffordancePosition,
+        testConfig: TestConfig,
+    ): KClass<*> {
+        val config =
+            when (position) {
+                KeyguardQuickAffordancePosition.BOTTOM_START -> homeControlsQuickAffordanceConfig
+                KeyguardQuickAffordancePosition.BOTTOM_END -> quickAccessWalletAffordanceConfig
+            }
+
+        affordanceRepository.setModel(
+            position = position,
+            model =
+                if (testConfig.isVisible) {
+                    if (testConfig.intent != null) {
+                        config.onClickedResult =
+                            KeyguardQuickAffordanceConfig.OnClickedResult.StartActivity(
+                                intent = testConfig.intent,
+                                canShowWhileLocked = testConfig.canShowWhileLocked,
+                            )
+                    }
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey = config::class,
+                        icon = testConfig.icon ?: error("Icon is unexpectedly null!"),
+                        contentDescriptionResourceId = CONTENT_DESCRIPTION_RESOURCE_ID,
+                    )
+                } else {
+                    KeyguardQuickAffordanceModel.Hidden
+                }
+        )
+        return config::class
+    }
+
+    private fun assertQuickAffordanceViewModel(
+        viewModel: KeyguardQuickAffordanceViewModel?,
+        testConfig: TestConfig,
+        configKey: KClass<*>,
+    ) {
+        checkNotNull(viewModel)
+        assertThat(viewModel.isVisible).isEqualTo(testConfig.isVisible)
+        if (testConfig.isVisible) {
+            assertThat(viewModel.icon).isEqualTo(testConfig.icon)
+            viewModel.onClicked.invoke(
+                KeyguardQuickAffordanceViewModel.OnClickedParameters(
+                    configKey = configKey,
+                    animationController = animationController,
+                )
+            )
+            testConfig.intent?.let { intent ->
+                assertThat(launchQuickAffordanceUseCase.invocations)
+                    .isEqualTo(
+                        listOf(
+                            FakeLaunchKeyguardQuickAffordanceUseCase.Invocation(
+                                intent = intent,
+                                canShowWhileLocked = testConfig.canShowWhileLocked,
+                                animationController = animationController,
+                            )
+                        )
+                    )
+            }
+                ?: run { assertThat(launchQuickAffordanceUseCase.invocations).isEmpty() }
+        } else {
+            assertThat(viewModel.isVisible).isFalse()
+        }
+    }
+
+    private data class TestConfig(
+        val isVisible: Boolean,
+        val icon: ContainedDrawable? = null,
+        val canShowWhileLocked: Boolean = false,
+        val intent: Intent? = null,
+    ) {
+        init {
+            check(!isVisible || icon != null) { "Must supply non-null icon if visible!" }
+        }
+    }
+
+    companion object {
+        private const val DEFAULT_BURN_IN_OFFSET = 5
+        private const val RETURNED_BURN_IN_OFFSET = 3
+        private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
new file mode 100644
index 0000000..4abb973
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/LogBufferTest.kt
@@ -0,0 +1,167 @@
+package com.android.systemui.log
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnitRunner
+
+@SmallTest
+@RunWith(MockitoJUnitRunner::class)
+class LogBufferTest : SysuiTestCase() {
+    private lateinit var buffer: LogBuffer
+
+    private lateinit var outputWriter: StringWriter
+
+    @Mock
+    private lateinit var logcatEchoTracker: LogcatEchoTracker
+
+    @Before
+    fun setup() {
+        outputWriter = StringWriter()
+        buffer = createBuffer(UNBOUNDED_STACK_TRACE, NESTED_TRACE_DEPTH)
+    }
+
+    private fun createBuffer(rootTraceDepth: Int, nestedTraceDepth: Int): LogBuffer {
+        return LogBuffer("TestBuffer",
+                1,
+                logcatEchoTracker,
+                false,
+                rootStackTraceDepth = rootTraceDepth,
+                nestedStackTraceDepth = nestedTraceDepth)
+    }
+
+    @Test
+    fun log_shouldSaveLogToBuffer() {
+        buffer.log("Test", LogLevel.INFO, "Some test message")
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("Some test message")
+    }
+
+    @Test
+    fun log_shouldRotateIfLogBufferIsFull() {
+        buffer.log("Test", LogLevel.INFO, "This should be rotated")
+        buffer.log("Test", LogLevel.INFO, "New test message")
+
+        val dumpedString = dumpBuffer()
+
+        assertThat(dumpedString).contains("New test message")
+    }
+
+    @Test
+    fun dump_writesExceptionAndStacktraceLimitedToGivenDepth() {
+        buffer = createBuffer(rootTraceDepth = 2, nestedTraceDepth = -1)
+        // stack trace depth of 5
+        val exception = createTestException("Exception message", "TestClass", 5)
+        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+
+        val dumpedString = dumpBuffer()
+
+        // logs are limited to depth 2
+        assertThat(dumpedString).contains("E Tag: Extra message")
+        assertThat(dumpedString).contains("E Tag: java.lang.RuntimeException: Exception message")
+        assertThat(dumpedString).contains("E Tag: \tat TestClass.TestMethod(TestClass.java:1)")
+        assertThat(dumpedString).contains("E Tag: \tat TestClass.TestMethod(TestClass.java:2)")
+        assertThat(dumpedString)
+                .doesNotContain("E Tag: \tat TestClass.TestMethod(TestClass.java:3)")
+    }
+
+    @Test
+    fun dump_writesCauseAndStacktraceLimitedToGivenDepth() {
+        buffer = createBuffer(rootTraceDepth = 0, nestedTraceDepth = 2)
+        val exception = createTestException("Exception message",
+                "TestClass",
+                1,
+                cause = createTestException("The real cause!", "TestClass", 5))
+        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+
+        val dumpedString = dumpBuffer()
+
+        // logs are limited to depth 2
+        assertThat(dumpedString)
+                .contains("E Tag: Caused by: java.lang.RuntimeException: The real cause!")
+        assertThat(dumpedString).contains("E Tag: \tat TestClass.TestMethod(TestClass.java:1)")
+        assertThat(dumpedString).contains("E Tag: \tat TestClass.TestMethod(TestClass.java:2)")
+        assertThat(dumpedString)
+                .doesNotContain("E Tag: \tat TestClass.TestMethod(TestClass.java:3)")
+    }
+
+    @Test
+    fun dump_writesSuppressedExceptionAndStacktraceLimitedToGivenDepth() {
+        buffer = createBuffer(rootTraceDepth = 0, nestedTraceDepth = 2)
+        val exception = RuntimeException("Root exception message")
+        exception.addSuppressed(
+                createTestException(
+                        "First suppressed exception",
+                        "FirstClass",
+                        5,
+                        createTestException("Cause of suppressed exp", "ThirdClass", 5)
+                ))
+        exception.addSuppressed(
+                createTestException("Second suppressed exception", "SecondClass", 5))
+        buffer.log("Tag", LogLevel.ERROR, { str1 = "Extra message" }, { str1!! }, exception)
+
+        val dumpedStr = dumpBuffer()
+
+        // logs are limited to depth 2
+        // first suppressed exception
+        assertThat(dumpedStr)
+                .contains("E Tag: Suppressed: " +
+                        "java.lang.RuntimeException: First suppressed exception")
+        assertThat(dumpedStr).contains("E Tag: \tat FirstClass.TestMethod(FirstClass.java:1)")
+        assertThat(dumpedStr).contains("E Tag: \tat FirstClass.TestMethod(FirstClass.java:2)")
+        assertThat(dumpedStr)
+                .doesNotContain("E Tag: \tat FirstClass.TestMethod(FirstClass.java:3)")
+
+        assertThat(dumpedStr)
+                .contains("E Tag: Caused by: java.lang.RuntimeException: Cause of suppressed exp")
+        assertThat(dumpedStr).contains("E Tag: \tat ThirdClass.TestMethod(ThirdClass.java:1)")
+        assertThat(dumpedStr).contains("E Tag: \tat ThirdClass.TestMethod(ThirdClass.java:2)")
+        assertThat(dumpedStr)
+                .doesNotContain("E Tag: \tat ThirdClass.TestMethod(ThirdClass.java:3)")
+
+        // second suppressed exception
+        assertThat(dumpedStr)
+                .contains("E Tag: Suppressed: " +
+                        "java.lang.RuntimeException: Second suppressed exception")
+        assertThat(dumpedStr).contains("E Tag: \tat SecondClass.TestMethod(SecondClass.java:1)")
+        assertThat(dumpedStr).contains("E Tag: \tat SecondClass.TestMethod(SecondClass.java:2)")
+        assertThat(dumpedStr)
+                .doesNotContain("E Tag: \tat SecondClass.TestMethod(SecondClass.java:3)")
+    }
+
+    private fun createTestException(
+        message: String,
+        errorClass: String,
+        stackTraceLength: Int,
+        cause: Throwable? = null
+    ): Exception {
+        val exception = RuntimeException(message, cause)
+        exception.stackTrace = createStackTraceElements(errorClass, stackTraceLength)
+        return exception
+    }
+
+    private fun dumpBuffer(): String {
+        buffer.dump(PrintWriter(outputWriter), tailLength = 100)
+        return outputWriter.toString()
+    }
+
+    private fun createStackTraceElements(
+        errorClass: String,
+        stackTraceLength: Int
+    ): Array<StackTraceElement> {
+        return (1..stackTraceLength).map { lineNumber ->
+            StackTraceElement(errorClass,
+                    "TestMethod",
+                    "$errorClass.java",
+                    lineNumber)
+        }.toTypedArray()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt
index 2eb4783..f133068 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/common/MediaTttChipControllerCommonTest.kt
@@ -21,7 +21,6 @@
 import android.content.pm.PackageManager
 import android.graphics.drawable.Drawable
 import android.os.PowerManager
-import android.view.MotionEvent
 import android.view.View
 import android.view.ViewGroup
 import android.view.WindowManager
@@ -31,7 +30,8 @@
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.statusbar.gesture.TapGestureDetector
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener
 import com.android.systemui.util.concurrency.DelayableExecutor
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
@@ -53,7 +53,7 @@
 
 @SmallTest
 class MediaTttChipControllerCommonTest : SysuiTestCase() {
-    private lateinit var controllerCommon: MediaTttChipControllerCommon<ChipInfo>
+    private lateinit var controllerCommon: TestControllerCommon
 
     private lateinit var fakeClock: FakeSystemClock
     private lateinit var fakeExecutor: FakeExecutor
@@ -68,12 +68,12 @@
     @Mock
     private lateinit var accessibilityManager: AccessibilityManager
     @Mock
+    private lateinit var configurationController: ConfigurationController
+    @Mock
     private lateinit var windowManager: WindowManager
     @Mock
     private lateinit var viewUtil: ViewUtil
     @Mock
-    private lateinit var tapGestureDetector: TapGestureDetector
-    @Mock
     private lateinit var powerManager: PowerManager
 
     @Before
@@ -98,35 +98,49 @@
         fakeExecutor = FakeExecutor(fakeClock)
 
         controllerCommon = TestControllerCommon(
-            context,
-            logger,
-            windowManager,
-            viewUtil,
-            fakeExecutor,
-            accessibilityManager,
-            tapGestureDetector,
-            powerManager
+                context,
+                logger,
+                windowManager,
+                viewUtil,
+                fakeExecutor,
+                accessibilityManager,
+                configurationController,
+                powerManager,
         )
     }
 
     @Test
-    fun displayChip_chipAddedAndGestureDetectionStartedAndScreenOn() {
+    fun displayChip_chipAdded() {
         controllerCommon.displayChip(getState())
 
         verify(windowManager).addView(any(), any())
-        verify(tapGestureDetector).addOnGestureDetectedCallback(any(), any())
+    }
+
+    @Test
+    fun displayChip_screenOff_screenWakes() {
+        whenever(powerManager.isScreenOn).thenReturn(false)
+
+        controllerCommon.displayChip(getState())
+
         verify(powerManager).wakeUp(any(), any(), any())
     }
 
     @Test
-    fun displayChip_twice_chipAndGestureDetectionNotAddedTwice() {
+    fun displayChip_screenAlreadyOn_screenNotWoken() {
+        whenever(powerManager.isScreenOn).thenReturn(true)
+
+        controllerCommon.displayChip(getState())
+
+        verify(powerManager, never()).wakeUp(any(), any(), any())
+    }
+
+    @Test
+    fun displayChip_twice_chipNotAddedTwice() {
         controllerCommon.displayChip(getState())
         reset(windowManager)
-        reset(tapGestureDetector)
 
         controllerCommon.displayChip(getState())
         verify(windowManager, never()).addView(any(), any())
-        verify(tapGestureDetector, never()).addOnGestureDetectedCallback(any(), any())
     }
 
     @Test
@@ -186,7 +200,20 @@
     }
 
     @Test
-    fun removeChip_chipRemovedAndGestureDetectionStoppedAndRemovalLogged() {
+    fun displayScaleChange_chipReinflatedWithMostRecentState() {
+        controllerCommon.displayChip(getState(name = "First name"))
+        controllerCommon.displayChip(getState(name = "Second name"))
+        reset(windowManager)
+
+        getConfigurationListener().onDensityOrFontScaleChanged()
+
+        verify(windowManager).removeView(any())
+        verify(windowManager).addView(any(), any())
+        assertThat(controllerCommon.mostRecentChipInfo?.name).isEqualTo("Second name")
+    }
+
+    @Test
+    fun removeChip_chipRemovedAndRemovalLogged() {
         // First, add the chip
         controllerCommon.displayChip(getState())
 
@@ -195,7 +222,6 @@
         controllerCommon.removeChip(reason)
 
         verify(windowManager).removeView(any())
-        verify(tapGestureDetector).removeOnGestureDetectedCallback(any())
         verify(logger).logChipRemoval(reason)
     }
 
@@ -307,41 +333,7 @@
         assertThat(chipView.getAppIconView().measuredHeight).isEqualTo(ICON_SIZE)
     }
 
-    @Test
-    fun tapGestureDetected_outsideViewBounds_viewHidden() {
-        controllerCommon.displayChip(getState())
-        whenever(viewUtil.touchIsWithinView(any(), any(), any())).thenReturn(false)
-        val gestureCallbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
-        verify(tapGestureDetector).addOnGestureDetectedCallback(
-            any(), capture(gestureCallbackCaptor)
-        )
-        val callback = gestureCallbackCaptor.value!!
-
-        callback.invoke(
-            MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
-        )
-
-        verify(windowManager).removeView(any())
-    }
-
-    @Test
-    fun tapGestureDetected_insideViewBounds_viewNotHidden() {
-        controllerCommon.displayChip(getState())
-        whenever(viewUtil.touchIsWithinView(any(), any(), any())).thenReturn(true)
-        val gestureCallbackCaptor = argumentCaptor<(MotionEvent) -> Unit>()
-        verify(tapGestureDetector).addOnGestureDetectedCallback(
-            any(), capture(gestureCallbackCaptor)
-        )
-        val callback = gestureCallbackCaptor.value!!
-
-        callback.invoke(
-            MotionEvent.obtain(0, 0, MotionEvent.ACTION_DOWN, 0f, 0f, 0)
-        )
-
-        verify(windowManager, never()).removeView(any())
-    }
-
-    private fun getState() = ChipInfo()
+    private fun getState(name: String = "name") = ChipInfo(name)
 
     private fun getChipView(): ViewGroup {
         val viewCaptor = ArgumentCaptor.forClass(View::class.java)
@@ -351,6 +343,12 @@
 
     private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
 
+    private fun getConfigurationListener(): ConfigurationListener {
+        val callbackCaptor = argumentCaptor<ConfigurationListener>()
+        verify(configurationController).addCallback(capture(callbackCaptor))
+        return callbackCaptor.value
+    }
+
     inner class TestControllerCommon(
         context: Context,
         logger: MediaTttLogger,
@@ -358,8 +356,8 @@
         viewUtil: ViewUtil,
         @Main mainExecutor: DelayableExecutor,
         accessibilityManager: AccessibilityManager,
-        tapGestureDetector: TapGestureDetector,
-        powerManager: PowerManager
+        configurationController: ConfigurationController,
+        powerManager: PowerManager,
     ) : MediaTttChipControllerCommon<ChipInfo>(
         context,
         logger,
@@ -367,16 +365,21 @@
         viewUtil,
         mainExecutor,
         accessibilityManager,
-        tapGestureDetector,
+        configurationController,
         powerManager,
-        R.layout.media_ttt_chip
+        R.layout.media_ttt_chip,
     ) {
+        var mostRecentChipInfo: ChipInfo? = null
+
         override val windowLayoutParams = commonWindowLayoutParams
-        override fun updateChipView(chipInfo: ChipInfo, currentChipView: ViewGroup) {}
+        override fun updateChipView(newChipInfo: ChipInfo, currentChipView: ViewGroup) {
+            super.updateChipView(newChipInfo, currentChipView)
+            mostRecentChipInfo = newChipInfo
+        }
         override fun getIconSize(isAppIcon: Boolean): Int = ICON_SIZE
     }
 
-    inner class ChipInfo : ChipInfoCommon {
+    inner class ChipInfo(val name: String) : ChipInfoCommon {
         override fun getTimeoutMs() = 1L
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
index bbc5641..dbc5f7c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiverTest.kt
@@ -36,7 +36,7 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.statusbar.gesture.TapGestureDetector
+import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
@@ -68,6 +68,8 @@
     @Mock
     private lateinit var accessibilityManager: AccessibilityManager
     @Mock
+    private lateinit var configurationController: ConfigurationController
+    @Mock
     private lateinit var powerManager: PowerManager
     @Mock
     private lateinit var windowManager: WindowManager
@@ -103,7 +105,7 @@
             viewUtil,
             FakeExecutor(FakeSystemClock()),
             accessibilityManager,
-            TapGestureDetector(context),
+            configurationController,
             powerManager,
             Handler.getMain(),
             receiverUiEventLogger
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
index 7ca0cd3..cd8ee73 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/taptotransfer/sender/MediaTttChipControllerSenderTest.kt
@@ -37,13 +37,12 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.media.taptotransfer.common.MediaTttLogger
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.statusbar.gesture.TapGestureDetector
+import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.FakeExecutor
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.time.FakeSystemClock
 import com.android.systemui.util.view.ViewUtil
-
 import com.google.common.truth.Truth.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -52,8 +51,8 @@
 import org.mockito.Mock
 import org.mockito.Mockito.never
 import org.mockito.Mockito.verify
-import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
+import org.mockito.Mockito.`when` as whenever
 
 @SmallTest
 @RunWith(AndroidTestingRunner::class)
@@ -70,6 +69,8 @@
     @Mock
     private lateinit var accessibilityManager: AccessibilityManager
     @Mock
+    private lateinit var configurationController: ConfigurationController
+    @Mock
     private lateinit var powerManager: PowerManager
     @Mock
     private lateinit var windowManager: WindowManager
@@ -112,7 +113,7 @@
             viewUtil,
             fakeExecutor,
             accessibilityManager,
-            TapGestureDetector(context),
+            configurationController,
             powerManager,
             senderUiEventLogger
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
index 4e3bdea..b0cf061 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarControllerTest.java
@@ -41,6 +41,7 @@
 import com.android.systemui.Dependency;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.model.SysUiState;
 import com.android.systemui.recents.OverviewProxyService;
 import com.android.systemui.statusbar.CommandQueue;
@@ -96,7 +97,8 @@
                         mock(AutoHideController.class),
                         mock(LightBarController.class),
                         Optional.of(mock(Pip.class)),
-                        Optional.of(mock(BackAnimation.class))));
+                        Optional.of(mock(BackAnimation.class)),
+                        mock(FeatureFlags.class)));
         initializeNavigationBars();
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierGroupControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierGroupControllerTest.java
index bd794d6..09ce37b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierGroupControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/carrier/QSCarrierGroupControllerTest.java
@@ -16,19 +16,26 @@
 
 package com.android.systemui.qs.carrier;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
+import android.content.Intent;
 import android.os.Handler;
+import android.provider.Settings;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
@@ -50,6 +57,7 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -85,6 +93,7 @@
     private QSCarrierGroupController.OnSingleCarrierChangedListener mOnSingleCarrierChangedListener;
 
     private FakeSlotIndexResolver mSlotIndexResolver;
+    private ClickListenerTextView mNoCarrierTextView;
 
     @Before
     public void setup() throws Exception {
@@ -108,7 +117,8 @@
                 .when(mCarrierTextManager)
                 .setListening(any(CarrierTextManager.CarrierTextCallback.class));
 
-        when(mQSCarrierGroup.getNoSimTextView()).thenReturn(new TextView(mContext));
+        mNoCarrierTextView = new ClickListenerTextView(mContext);
+        when(mQSCarrierGroup.getNoSimTextView()).thenReturn(mNoCarrierTextView);
         when(mQSCarrierGroup.getCarrier1View()).thenReturn(mQSCarrier1);
         when(mQSCarrierGroup.getCarrier2View()).thenReturn(mQSCarrier2);
         when(mQSCarrierGroup.getCarrier3View()).thenReturn(mQSCarrier3);
@@ -376,6 +386,47 @@
         verify(mOnSingleCarrierChangedListener, never()).onSingleCarrierChanged(anyBoolean());
     }
 
+    @Test
+    public void testOnlyInternalViewsHaveClickableListener() {
+        ArgumentCaptor<View.OnClickListener> captor =
+                ArgumentCaptor.forClass(View.OnClickListener.class);
+
+        verify(mQSCarrier1).setOnClickListener(captor.capture());
+        verify(mQSCarrier2).setOnClickListener(captor.getValue());
+        verify(mQSCarrier3).setOnClickListener(captor.getValue());
+
+        assertThat(mNoCarrierTextView.getOnClickListener()).isSameInstanceAs(captor.getValue());
+        verify(mQSCarrierGroup, never()).setOnClickListener(any());
+    }
+
+    @Test
+    public void testOnClickListenerDoesntStartActivityIfViewNotVisible() {
+        ArgumentCaptor<View.OnClickListener> captor =
+                ArgumentCaptor.forClass(View.OnClickListener.class);
+
+        verify(mQSCarrier1).setOnClickListener(captor.capture());
+        when(mQSCarrier1.isVisibleToUser()).thenReturn(false);
+
+        captor.getValue().onClick(mQSCarrier1);
+        verifyZeroInteractions(mActivityStarter);
+    }
+
+    @Test
+    public void testOnClickListenerLaunchesActivityIfViewVisible() {
+        ArgumentCaptor<View.OnClickListener> listenerCaptor =
+                ArgumentCaptor.forClass(View.OnClickListener.class);
+        ArgumentCaptor<Intent> intentCaptor = ArgumentCaptor.forClass(Intent.class);
+
+        verify(mQSCarrier1).setOnClickListener(listenerCaptor.capture());
+        when(mQSCarrier1.isVisibleToUser()).thenReturn(true);
+
+        listenerCaptor.getValue().onClick(mQSCarrier1);
+        verify(mActivityStarter)
+                .postStartActivityDismissingKeyguard(intentCaptor.capture(), anyInt());
+        assertThat(intentCaptor.getValue().getAction())
+                .isEqualTo(Settings.ACTION_WIRELESS_SETTINGS);
+    }
+
     private class FakeSlotIndexResolver implements QSCarrierGroupController.SlotIndexResolver {
         public boolean overrideInvalid;
 
@@ -384,4 +435,22 @@
             return overrideInvalid ? -1 : subscriptionId;
         }
     }
+
+    private class ClickListenerTextView extends TextView {
+        View.OnClickListener mListener = null;
+
+        ClickListenerTextView(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void setOnClickListener(OnClickListener l) {
+            super.setOnClickListener(l);
+            mListener = l;
+        }
+
+        View.OnClickListener getOnClickListener() {
+            return mListener;
+        }
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index e2673bb..c9405c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -97,6 +97,10 @@
 import com.android.systemui.fragments.FragmentHostManager;
 import com.android.systemui.fragments.FragmentService;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.domain.usecase.SetClockPositionUseCase;
+import com.android.systemui.keyguard.domain.usecase.SetKeyguardBottomAreaAlphaUseCase;
+import com.android.systemui.keyguard.domain.usecase.SetKeyguardBottomAreaAnimateDozingTransitionsUseCase;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel;
 import com.android.systemui.media.KeyguardMediaController;
 import com.android.systemui.media.MediaDataManager;
 import com.android.systemui.media.MediaHierarchyManager;
@@ -373,6 +377,11 @@
     private ViewParent mViewParent;
     @Mock
     private ViewTreeObserver mViewTreeObserver;
+    @Mock private KeyguardBottomAreaViewModel mKeyguardBottomAreaViewModel;
+    @Mock private SetClockPositionUseCase mSetClockPositionUseCase;
+    @Mock private SetKeyguardBottomAreaAlphaUseCase mSetKeyguardBottomAreaAlphaUseCase;
+    @Mock private SetKeyguardBottomAreaAnimateDozingTransitionsUseCase
+            mSetKeyguardBottomAreaAnimateDozingTransitionsUseCase;
     private NotificationPanelViewController.PanelEventsEmitter mPanelEventsEmitter;
     private Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty();
     private SysuiStatusBarStateController mStatusBarStateController;
@@ -564,7 +573,11 @@
                 mUnlockedScreenOffAnimationController,
                 mShadeTransitionController,
                 mSystemClock,
-                mock(CameraGestureHelper.class));
+                mock(CameraGestureHelper.class),
+                () -> mKeyguardBottomAreaViewModel,
+                () -> mSetClockPositionUseCase,
+                () -> mSetKeyguardBottomAreaAlphaUseCase,
+                () -> mSetKeyguardBottomAreaAnimateDozingTransitionsUseCase);
         mNotificationPanelViewController.initDependencies(
                 mCentralSurfaces,
                 () -> {},
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
index d61989f..131eac6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt
@@ -110,6 +110,7 @@
                 get() = settingValue
                 set(value) { settingValue = value }
         }
+        registry.isEnabled = true
 
         verify(mockPluginManager)
             .addPluginListener(captor.capture(), eq(ClockProviderPlugin::class.java))
@@ -129,13 +130,16 @@
         pluginListener.onPluginConnected(plugin1, mockContext)
         pluginListener.onPluginConnected(plugin2, mockContext)
         val list = registry.getClocks()
-        assertEquals(list, listOf(
-            ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
-            ClockMetadata("clock_1", "clock 1"),
-            ClockMetadata("clock_2", "clock 2"),
-            ClockMetadata("clock_3", "clock 3"),
-            ClockMetadata("clock_4", "clock 4")
-        ))
+        assertEquals(
+            list,
+            listOf(
+                ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
+                ClockMetadata("clock_1", "clock 1"),
+                ClockMetadata("clock_2", "clock 2"),
+                ClockMetadata("clock_3", "clock 3"),
+                ClockMetadata("clock_4", "clock 4")
+            )
+        )
     }
 
     @Test
@@ -157,11 +161,14 @@
         pluginListener.onPluginConnected(plugin1, mockContext)
         pluginListener.onPluginConnected(plugin2, mockContext)
         val list = registry.getClocks()
-        assertEquals(list, listOf(
-            ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
-            ClockMetadata("clock_1", "clock 1"),
-            ClockMetadata("clock_2", "clock 2")
-        ))
+        assertEquals(
+            list,
+            listOf(
+                ClockMetadata(DEFAULT_CLOCK_ID, DEFAULT_CLOCK_NAME),
+                ClockMetadata("clock_1", "clock 1"),
+                ClockMetadata("clock_2", "clock 2")
+            )
+        )
 
         assertEquals(registry.createExampleClock("clock_1"), mockClock)
         assertEquals(registry.createExampleClock("clock_2"), mockClock)
@@ -221,7 +228,7 @@
         pluginListener.onPluginConnected(plugin2, mockContext)
 
         var changeCallCount = 0
-        registry.registerClockChangeListener({ changeCallCount++ })
+        registry.registerClockChangeListener { changeCallCount++ }
 
         pluginListener.onPluginDisconnected(plugin1)
         assertEquals(0, changeCallCount)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
index b2ef7b3..0fd2e38 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NonPhoneDependencyTest.java
@@ -30,13 +30,11 @@
 import com.android.systemui.shade.ShadeController;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener;
 import com.android.systemui.statusbar.notification.row.NotificationInfo.CheckSaveListener;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -55,11 +53,8 @@
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 public class NonPhoneDependencyTest extends SysuiTestCase {
     @Mock private NotificationPresenter mPresenter;
-    @Mock private NotifStackController mStackController;
     @Mock private NotificationListContainer mListContainer;
-    @Mock
-    private NotificationEntryListener mEntryListener;
-    @Mock private HeadsUpManager mHeadsUpManager;
+    @Mock private NotificationEntryListener mEntryListener;
     @Mock private RemoteInputController.Delegate mDelegate;
     @Mock private NotificationRemoteInputManager.Callback mRemoteInputManagerCallback;
     @Mock private CheckSaveListener mCheckSaveListener;
@@ -79,25 +74,20 @@
         mDependency.injectMockDependency(ShadeController.class);
         NotificationEntryManager entryManager = Dependency.get(NotificationEntryManager.class);
         NotificationGutsManager gutsManager = Dependency.get(NotificationGutsManager.class);
-        NotificationListener notificationListener = Dependency.get(NotificationListener.class);
         NotificationLogger notificationLogger = Dependency.get(NotificationLogger.class);
         NotificationMediaManager mediaManager = Dependency.get(NotificationMediaManager.class);
         NotificationRemoteInputManager remoteInputManager =
                 Dependency.get(NotificationRemoteInputManager.class);
         NotificationLockscreenUserManager lockscreenUserManager =
                 Dependency.get(NotificationLockscreenUserManager.class);
-        NotificationViewHierarchyManager viewHierarchyManager =
-                Dependency.get(NotificationViewHierarchyManager.class);
-        entryManager.setUpWithPresenter(mPresenter);
         entryManager.addNotificationEntryListener(mEntryListener);
         gutsManager.setUpWithPresenter(mPresenter, mListContainer,
-                mCheckSaveListener, mOnSettingsClickListener);
+                mOnSettingsClickListener);
         notificationLogger.setUpWithContainer(mListContainer);
         mediaManager.setUpWithPresenter(mPresenter);
         remoteInputManager.setUpWithCallback(mRemoteInputManagerCallback,
                 mDelegate);
         lockscreenUserManager.setUpWithPresenter(mPresenter);
-        viewHierarchyManager.setUpWithPresenter(mPresenter, mStackController, mListContainer);
 
         TestableLooper.get(this).processAllMessages();
         assertFalse(mDependency.hasInstantiatedDependency(NotificationShadeWindowController.class));
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
deleted file mode 100644
index 407044b..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
+++ /dev/null
@@ -1,375 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar;
-
-import static junit.framework.Assert.assertTrue;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.Mockito.clearInvocations;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.os.Handler;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper;
-import com.android.systemui.statusbar.notification.AssistantFeedbackController;
-import com.android.systemui.statusbar.notification.DynamicChildBindController;
-import com.android.systemui.statusbar.notification.DynamicPrivacyController;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
-import com.android.systemui.statusbar.notification.NotificationActivityStarter;
-import com.android.systemui.statusbar.notification.NotificationEntryManager;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.legacy.LowPriorityInflationHelper;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
-import com.android.systemui.statusbar.notification.collection.legacy.VisualStabilityManager;
-import com.android.systemui.statusbar.notification.collection.render.NotifStackController;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.notification.row.ExpandableView;
-import com.android.systemui.statusbar.notification.row.NotificationTestHelper;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.wm.shell.bubbles.Bubbles;
-
-import com.google.android.collect.Lists;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.mockito.Spy;
-
-import java.util.List;
-import java.util.Optional;
-
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper
-public class NotificationViewHierarchyManagerTest extends SysuiTestCase {
-    @Mock private NotificationPresenter mPresenter;
-    @Mock private NotifStackController mStackController;
-    @Spy private FakeListContainer mListContainer = new FakeListContainer();
-
-    // Dependency mocks:
-    @Mock private FeatureFlags mFeatureFlags;
-    @Mock private NotifPipelineFlags mNotifPipelineFlags;
-    @Mock private NotificationEntryManager mEntryManager;
-    @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
-    @Mock private NotificationGroupManagerLegacy mGroupManager;
-    @Mock private VisualStabilityManager mVisualStabilityManager;
-
-    private TestableLooper mTestableLooper;
-    private Handler mHandler;
-    private NotificationViewHierarchyManager mViewHierarchyManager;
-    private NotificationTestHelper mHelper;
-    private boolean mMadeReentrantCall = false;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mTestableLooper = TestableLooper.get(this);
-        allowTestableLooperAsMainThread();
-        mHandler = Handler.createAsync(mTestableLooper.getLooper());
-
-        mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
-        mDependency.injectTestDependency(NotificationLockscreenUserManager.class,
-                mLockscreenUserManager);
-        mDependency.injectTestDependency(NotificationGroupManagerLegacy.class, mGroupManager);
-        mDependency.injectTestDependency(VisualStabilityManager.class, mVisualStabilityManager);
-        when(mVisualStabilityManager.areGroupChangesAllowed()).thenReturn(true);
-        when(mVisualStabilityManager.isReorderingAllowed()).thenReturn(true);
-
-        when(mNotifPipelineFlags.checkLegacyPipelineEnabled()).thenReturn(true);
-        when(mNotifPipelineFlags.isNewPipelineEnabled()).thenReturn(false);
-
-        mHelper = new NotificationTestHelper(mContext, mDependency, TestableLooper.get(this));
-
-        mViewHierarchyManager = new NotificationViewHierarchyManager(mContext,
-                mHandler, mFeatureFlags, mLockscreenUserManager, mGroupManager,
-                mVisualStabilityManager,
-                mock(StatusBarStateControllerImpl.class), mEntryManager,
-                mock(KeyguardBypassController.class),
-                Optional.of(mock(Bubbles.class)),
-                mock(DynamicPrivacyController.class),
-                mock(DynamicChildBindController.class),
-                mock(LowPriorityInflationHelper.class),
-                mock(AssistantFeedbackController.class),
-                mNotifPipelineFlags,
-                mock(KeyguardUpdateMonitor.class),
-                mock(KeyguardStateController.class));
-        mViewHierarchyManager.setUpWithPresenter(mPresenter, mStackController, mListContainer);
-    }
-
-    private NotificationEntry createEntry() throws Exception {
-        ExpandableNotificationRow row = mHelper.createRow();
-        return row.getEntry();
-    }
-
-    @Test
-    public void testNotificationsBecomingBundled() throws Exception {
-        // Tests 3 top level notifications becoming a single bundled notification with |entry0| as
-        // the summary.
-        NotificationEntry entry0 = createEntry();
-        NotificationEntry entry1 = createEntry();
-        NotificationEntry entry2 = createEntry();
-
-        // Set up the prior state to look like three top level notifications.
-        mListContainer.addContainerView(entry0.getRow());
-        mListContainer.addContainerView(entry1.getRow());
-        mListContainer.addContainerView(entry2.getRow());
-        when(mEntryManager.getVisibleNotifications()).thenReturn(
-                Lists.newArrayList(entry0, entry1, entry2));
-
-        // Set up group manager to report that they should be bundled now.
-        when(mGroupManager.isChildInGroup(entry0)).thenReturn(false);
-        when(mGroupManager.isChildInGroup(entry1)).thenReturn(true);
-        when(mGroupManager.isChildInGroup(entry2)).thenReturn(true);
-        when(mGroupManager.getGroupSummary(entry1)).thenReturn(entry0);
-        when(mGroupManager.getGroupSummary(entry2)).thenReturn(entry0);
-
-        // Run updateNotifications - the view hierarchy should be reorganized.
-        mViewHierarchyManager.updateNotificationViews();
-
-        verify(mListContainer).notifyGroupChildAdded(entry1.getRow());
-        verify(mListContainer).notifyGroupChildAdded(entry2.getRow());
-        assertTrue(Lists.newArrayList(entry0.getRow()).equals(mListContainer.mRows));
-    }
-
-    @Test
-    public void testNotificationsBecomingUnbundled() throws Exception {
-        // Tests a bundled notification becoming three top level notifications.
-        NotificationEntry entry0 = createEntry();
-        NotificationEntry entry1 = createEntry();
-        NotificationEntry entry2 = createEntry();
-        entry0.getRow().addChildNotification(entry1.getRow());
-        entry0.getRow().addChildNotification(entry2.getRow());
-
-        // Set up the prior state to look like one top level notification.
-        mListContainer.addContainerView(entry0.getRow());
-        when(mEntryManager.getVisibleNotifications()).thenReturn(
-                Lists.newArrayList(entry0, entry1, entry2));
-
-        // Set up group manager to report that they should not be bundled now.
-        when(mGroupManager.isChildInGroup(entry0)).thenReturn(false);
-        when(mGroupManager.isChildInGroup(entry1)).thenReturn(false);
-        when(mGroupManager.isChildInGroup(entry2)).thenReturn(false);
-
-        // Run updateNotifications - the view hierarchy should be reorganized.
-        mViewHierarchyManager.updateNotificationViews();
-
-        verify(mListContainer).notifyGroupChildRemoved(
-                entry1.getRow(), entry0.getRow().getChildrenContainer());
-        verify(mListContainer).notifyGroupChildRemoved(
-                entry2.getRow(), entry0.getRow().getChildrenContainer());
-        assertTrue(
-                Lists.newArrayList(entry0.getRow(), entry1.getRow(), entry2.getRow())
-                        .equals(mListContainer.mRows));
-    }
-
-    @Test
-    public void testNotificationsBecomingSuppressed() throws Exception {
-        // Tests two top level notifications becoming a suppressed summary and a child.
-        NotificationEntry entry0 = createEntry();
-        NotificationEntry entry1 = createEntry();
-        entry0.getRow().addChildNotification(entry1.getRow());
-
-        // Set up the prior state to look like a top level notification.
-        mListContainer.addContainerView(entry0.getRow());
-        when(mEntryManager.getVisibleNotifications()).thenReturn(
-                Lists.newArrayList(entry0, entry1));
-
-        // Set up group manager to report a suppressed summary now.
-        when(mGroupManager.isChildInGroup(entry0)).thenReturn(false);
-        when(mGroupManager.isChildInGroup(entry1)).thenReturn(false);
-        when(mGroupManager.isSummaryOfSuppressedGroup(entry0.getSbn())).thenReturn(true);
-
-        // Run updateNotifications - the view hierarchy should be reorganized.
-        mViewHierarchyManager.updateNotificationViews();
-
-        verify(mListContainer).notifyGroupChildRemoved(
-                entry1.getRow(), entry0.getRow().getChildrenContainer());
-        assertTrue(Lists.newArrayList(entry0.getRow(), entry1.getRow()).equals(mListContainer.mRows));
-        assertEquals(View.GONE, entry0.getRow().getVisibility());
-        assertEquals(View.VISIBLE, entry1.getRow().getVisibility());
-    }
-
-    @Test
-    public void testReentrantCallsToOnDynamicPrivacyChangedPostForLater() {
-        // GIVEN a ListContainer that will make a re-entrant call to updateNotificationViews()
-        mMadeReentrantCall = false;
-        doAnswer((invocation) -> {
-            if (!mMadeReentrantCall) {
-                mMadeReentrantCall = true;
-                mViewHierarchyManager.onDynamicPrivacyChanged();
-            }
-            return null;
-        }).when(mListContainer).onNotificationViewUpdateFinished();
-
-        // WHEN we call updateNotificationViews()
-        mViewHierarchyManager.updateNotificationViews();
-
-        // THEN onNotificationViewUpdateFinished() is only called once
-        verify(mListContainer).onNotificationViewUpdateFinished();
-
-        // WHEN we drain the looper
-        mTestableLooper.processAllMessages();
-
-        // THEN updateNotificationViews() is called a second time (for the reentrant call)
-        verify(mListContainer, times(2)).onNotificationViewUpdateFinished();
-    }
-
-    @Test
-    public void testMultipleReentrantCallsToOnDynamicPrivacyChangedOnlyPostOnce() {
-        // GIVEN a ListContainer that will make many re-entrant calls to updateNotificationViews()
-        mMadeReentrantCall = false;
-        doAnswer((invocation) -> {
-            if (!mMadeReentrantCall) {
-                mMadeReentrantCall = true;
-                mViewHierarchyManager.onDynamicPrivacyChanged();
-                mViewHierarchyManager.onDynamicPrivacyChanged();
-                mViewHierarchyManager.onDynamicPrivacyChanged();
-                mViewHierarchyManager.onDynamicPrivacyChanged();
-            }
-            return null;
-        }).when(mListContainer).onNotificationViewUpdateFinished();
-
-        // WHEN we call updateNotificationViews() and drain the looper
-        mViewHierarchyManager.updateNotificationViews();
-        verify(mListContainer).onNotificationViewUpdateFinished();
-        clearInvocations(mListContainer);
-        mTestableLooper.processAllMessages();
-
-        // THEN updateNotificationViews() is called only one more time
-        verify(mListContainer).onNotificationViewUpdateFinished();
-    }
-
-    private class FakeListContainer implements NotificationListContainer {
-        final LinearLayout mLayout = new LinearLayout(mContext);
-        final List<View> mRows = Lists.newArrayList();
-
-        @Override
-        public void setChildTransferInProgress(boolean childTransferInProgress) {}
-
-        @Override
-        public void changeViewPosition(ExpandableView child, int newIndex) {
-            mRows.remove(child);
-            mRows.add(newIndex, child);
-        }
-
-        @Override
-        public void notifyGroupChildAdded(ExpandableView row) {}
-
-        @Override
-        public void notifyGroupChildRemoved(ExpandableView row, ViewGroup childrenContainer) {}
-
-        @Override
-        public void generateAddAnimation(ExpandableView child, boolean fromMoreCard) {}
-
-        @Override
-        public void generateChildOrderChangedEvent() {}
-
-        @Override
-        public void onReset(ExpandableView view) {}
-
-        @Override
-        public int getContainerChildCount() {
-            return mRows.size();
-        }
-
-        @Override
-        public View getContainerChildAt(int i) {
-            return mRows.get(i);
-        }
-
-        @Override
-        public void removeContainerView(View v) {
-            mLayout.removeView(v);
-            mRows.remove(v);
-        }
-
-        @Override
-        public void setNotificationActivityStarter(
-                NotificationActivityStarter notificationActivityStarter) {}
-
-        @Override
-        public void addContainerView(View v) {
-            mLayout.addView(v);
-            mRows.add(v);
-        }
-
-        @Override
-        public void addContainerViewAt(View v, int index) {
-            mLayout.addView(v, index);
-            mRows.add(index, v);
-        }
-
-        @Override
-        public void setMaxDisplayedNotifications(int maxNotifications) {
-        }
-
-        @Override
-        public ViewGroup getViewParentForNotification(NotificationEntry entry) {
-            return null;
-        }
-
-        @Override
-        public void onHeightChanged(ExpandableView view, boolean animate) {}
-
-        @Override
-        public void resetExposedMenuView(boolean animate, boolean force) {}
-
-        @Override
-        public NotificationSwipeActionHelper getSwipeActionHelper() {
-            return null;
-        }
-
-        @Override
-        public void cleanUpViewStateForEntry(NotificationEntry entry) { }
-
-        @Override
-        public boolean isInVisibleLocation(NotificationEntry entry) {
-            return true;
-        }
-
-        @Override
-        public void setChildLocationsChangedListener(
-                NotificationLogger.OnChildLocationsChangedListener listener) {}
-
-        @Override
-        public boolean hasPulsingNotifications() {
-            return false;
-        }
-
-        @Override
-        public void onNotificationViewUpdateFinished() { }
-    }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
index aeef6b0..842f057 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
@@ -77,10 +77,8 @@
 import com.android.systemui.statusbar.NotificationPresenter;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationRemoveInterceptor;
-import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SmartReplyController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager.KeyguardEnvironment;
-import com.android.systemui.statusbar.notification.collection.NotifLiveDataStoreMocksKt;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
 import com.android.systemui.statusbar.notification.collection.NotificationRankingManager;
@@ -91,7 +89,6 @@
 import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
 import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.notification.row.NotificationEntryManagerInflationTest;
 import com.android.systemui.statusbar.notification.row.RowInflaterTask;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
 import com.android.systemui.statusbar.policy.HeadsUpManager;
@@ -115,9 +112,7 @@
 import java.util.Set;
 
 /**
- * Unit tests for {@link NotificationEntryManager}. This test will not test any interactions with
- * inflation. Instead, for functional inflation tests, see
- * {@link NotificationEntryManagerInflationTest}.
+ * Unit tests for {@link NotificationEntryManager}.
  */
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
@@ -205,7 +200,6 @@
         mStats = defaultStats(mEntry);
         mSbn = mEntry.getSbn();
 
-        when(mNotifPipelineFlags.isNewPipelineEnabled()).thenReturn(false);
         mEntryManager = new NotificationEntryManager(
                 mLogger,
                 mGroupManager,
@@ -214,7 +208,6 @@
                 () -> mRemoteInputManager,
                 mLeakDetector,
                 mStatusBarService,
-                NotifLiveDataStoreMocksKt.createNotifLiveDataStoreImplMock(),
                 mock(DumpManager.class),
                 mBgExecutor
         );
@@ -230,7 +223,6 @@
                         mock(PeopleNotificationIdentifier.class),
                         mock(HighPriorityProvider.class),
                         mEnvironment));
-        mEntryManager.setUpWithPresenter(mPresenter);
         mEntryManager.addNotificationEntryListener(mEntryListener);
         mEntryManager.addCollectionListener(mNotifCollectionListener);
         mEntryManager.addNotificationRemoveInterceptor(mRemoveInterceptor);
@@ -273,17 +265,6 @@
     }
 
     @Test
-    public void testUpdateNotification_updatesUserSentiment() {
-        mEntryManager.addActiveNotificationForTest(mEntry);
-        setUserSentiment(
-                mEntry.getKey(), Ranking.USER_SENTIMENT_NEGATIVE);
-
-        mEntryManager.updateNotification(mSbn, mRankingMap);
-
-        assertEquals(Ranking.USER_SENTIMENT_NEGATIVE, mEntry.getUserSentiment());
-    }
-
-    @Test
     public void testUpdateNotification_prePostEntryOrder() throws Exception {
         TestableLooper.get(this).processAllMessages();
 
@@ -294,7 +275,6 @@
         // Ensure that update callbacks happen in correct order
         InOrder order = inOrder(mEntryListener, mPresenter, mEntryListener);
         order.verify(mEntryListener).onPreEntryUpdated(mEntry);
-        order.verify(mPresenter).updateNotificationViews(any());
         order.verify(mEntryListener).onPostEntryUpdated(mEntry);
     }
 
@@ -305,7 +285,6 @@
 
         mEntryManager.removeNotification(mSbn.getKey(), mRankingMap, UNDEFINED_DISMISS_REASON);
 
-        verify(mPresenter).updateNotificationViews(any());
         verify(mEntryListener).onEntryRemoved(
                 argThat(matchEntryOnKey()), any(),
                 eq(false) /* removedByUser */, eq(UNDEFINED_DISMISS_REASON));
@@ -379,23 +358,6 @@
     }
 
     @Test
-    public void testUpdateNotificationRanking() {
-        when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
-        when(mEnvironment.isDeviceProvisioned()).thenReturn(true);
-        when(mEnvironment.isNotificationForCurrentProfiles(any())).thenReturn(true);
-
-        mEntry.setRow(mRow);
-        mEntry.setInflationTask(mAsyncInflationTask);
-        mEntryManager.addActiveNotificationForTest(mEntry);
-        setSmartActions(mEntry.getKey(), new ArrayList<>(Arrays.asList(createAction())));
-
-        mEntryManager.updateNotificationRanking(mRankingMap);
-        assertEquals(1, mEntry.getSmartActions().size());
-        assertEquals("action", mEntry.getSmartActions().get(0).title);
-        verify(mEntryListener).onNotificationRankingUpdated(mRankingMap);
-    }
-
-    @Test
     public void testUpdateNotificationRanking_noChange() {
         when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
         when(mEnvironment.isNotificationForCurrentProfiles(any())).thenReturn(true);
@@ -409,20 +371,6 @@
     }
 
     @Test
-    public void testUpdateNotificationRanking_rowNotInflatedYet() {
-        when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
-        when(mEnvironment.isNotificationForCurrentProfiles(any())).thenReturn(true);
-
-        mEntry.setRow(null);
-        mEntryManager.addActiveNotificationForTest(mEntry);
-        setSmartActions(mEntry.getKey(), new ArrayList<>(Arrays.asList(createAction())));
-
-        mEntryManager.updateNotificationRanking(mRankingMap);
-        assertEquals(1, mEntry.getSmartActions().size());
-        assertEquals("action", mEntry.getSmartActions().get(0).title);
-    }
-
-    @Test
     public void testUpdateNotificationRanking_pendingNotification() {
         when(mDeviceProvisionedController.isDeviceProvisioned()).thenReturn(true);
         when(mEnvironment.isNotificationForCurrentProfiles(any())).thenReturn(true);
@@ -614,31 +562,6 @@
     /* Tests annexed from NotificationDataTest go here */
 
     @Test
-    public void testChannelIsSetWhenAdded() {
-        NotificationChannel nc = new NotificationChannel(
-                "testId",
-                "testName",
-                IMPORTANCE_DEFAULT);
-
-        Ranking r = new RankingBuilder()
-                .setKey(mEntry.getKey())
-                .setChannel(nc)
-                .build();
-
-        RankingMap rm = new RankingMap(new Ranking[] { r });
-
-        // GIVEN: a notification is added, and the ranking updated
-        mEntryManager.addActiveNotificationForTest(mEntry);
-        mEntryManager.updateRanking(rm, "testReason");
-
-        // THEN the notification entry better have a channel on it
-        assertEquals(
-                "Channel must be set when adding a notification",
-                nc.getName(),
-                mEntry.getChannel().getName());
-    }
-
-    @Test
     public void testGetNotificationsForCurrentUser_shouldFilterNonCurrentUserNotifications() {
         Notification.Builder n = new Notification.Builder(mContext, "di")
                 .setSmallIcon(R.drawable.ic_person)
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 c283cec..dfa38ab 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
@@ -33,7 +33,6 @@
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
 import static org.mockito.Mockito.inOrder;
-import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.times;
@@ -1846,103 +1845,6 @@
     }
 
     @Test
-    public void stableOrderingDisregardedWithSectionChange() {
-        // GIVEN the first sectioner's packages can be changed from run-to-run
-        List<String> mutableSectionerPackages = new ArrayList<>();
-        mutableSectionerPackages.add(PACKAGE_1);
-        mListBuilder.setSectioners(asList(
-                new PackageSectioner(mutableSectionerPackages, null),
-                new PackageSectioner(List.of(PACKAGE_1, PACKAGE_2, PACKAGE_3), null)));
-        mStabilityManager.setAllowEntryReordering(false);
-
-        // WHEN the list is originally built with reordering disabled (and section changes allowed)
-        addNotif(0, PACKAGE_1).setRank(1);
-        addNotif(1, PACKAGE_1).setRank(5);
-        addNotif(2, PACKAGE_2).setRank(2);
-        addNotif(3, PACKAGE_2).setRank(3);
-        addNotif(4, PACKAGE_3).setRank(4);
-        dispatchBuild();
-
-        // VERIFY the order and that entry reordering has not been suppressed
-        verifyBuiltList(
-                notif(0),
-                notif(1),
-                notif(2),
-                notif(3),
-                notif(4)
-        );
-        verify(mStabilityManager, never()).onEntryReorderSuppressed();
-
-        // WHEN the first section now claims PACKAGE_3 notifications
-        mutableSectionerPackages.add(PACKAGE_3);
-        dispatchBuild();
-
-        // VERIFY the re-sectioned notification is inserted at the top of the first section, because
-        // it's effectively "new" and "new" things are inserted at the top of their section.
-        verifyBuiltList(
-                notif(4),
-                notif(0),
-                notif(1),
-                notif(2),
-                notif(3)
-        );
-        verify(mStabilityManager).onEntryReorderSuppressed();
-        clearInvocations(mStabilityManager);
-
-        // WHEN reordering is now allowed again
-        mStabilityManager.setAllowEntryReordering(true);
-        dispatchBuild();
-
-        // VERIFY that list order changes to put the re-sectioned notification in the middle where
-        // it is ranked.
-        verifyBuiltList(
-                notif(0),
-                notif(4),
-                notif(1),
-                notif(2),
-                notif(3)
-        );
-        verify(mStabilityManager, never()).onEntryReorderSuppressed();
-    }
-
-    @Test
-    public void groupRevertingToSummaryRetainsStablePosition() {
-        // GIVEN a notification group is on screen
-        mStabilityManager.setAllowEntryReordering(false);
-
-        // WHEN the list is originally built with reordering disabled (and section changes allowed)
-        addNotif(0, PACKAGE_1).setRank(2);
-        addNotif(1, PACKAGE_1).setRank(3);
-        addGroupSummary(2, PACKAGE_1, "group").setRank(4);
-        addGroupChild(3, PACKAGE_1, "group").setRank(5);
-        addGroupChild(4, PACKAGE_1, "group").setRank(6);
-        dispatchBuild();
-
-        verifyBuiltList(
-                notif(0),
-                notif(1),
-                group(
-                        summary(2),
-                        child(3),
-                        child(4)
-                )
-        );
-
-        // WHEN the notification summary rank increases and children removed
-        setNewRank(notif(2).entry, 1);
-        mEntrySet.remove(4);
-        mEntrySet.remove(3);
-        dispatchBuild();
-
-        // VERIFY the summary stays in the same location on rebuild
-        verifyBuiltList(
-                notif(0),
-                notif(1),
-                notif(2)
-        );
-    }
-
-    @Test
     public void testStableChildOrdering() {
         // WHEN the list is originally built with reordering disabled
         mStabilityManager.setAllowEntryReordering(false);
@@ -2138,7 +2040,6 @@
 
     private void assertOrder(String visible, String active, String expected) {
         StringBuilder differenceSb = new StringBuilder();
-        NotifSection section = new NotifSection(mock(NotifSectioner.class), 0);
         for (char c : active.toCharArray()) {
             if (visible.indexOf(c) < 0) differenceSb.append(c);
         }
@@ -2147,7 +2048,6 @@
         for (int i = 0; i < visible.length(); i++) {
             addNotif(i, String.valueOf(visible.charAt(i)))
                     .setRank(active.indexOf(visible.charAt(i)))
-                    .setSection(section)
                     .setStableIndex(i);
 
         }
@@ -2155,7 +2055,6 @@
         for (int i = 0; i < difference.length(); i++) {
             addNotif(i + visible.length(), String.valueOf(difference.charAt(i)))
                     .setRank(active.indexOf(difference.charAt(i)))
-                    .setSection(section)
                     .setStableIndex(-1);
         }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
index 5386171..e00e20f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImplTest.java
@@ -118,7 +118,6 @@
                         mPowerManager,
                         mDreamManager,
                         mAmbientDisplayConfiguration,
-                        mNotificationFilter,
                         mBatteryController,
                         mStatusBarStateController,
                         mKeyguardStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
deleted file mode 100644
index bf7549a..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java
+++ /dev/null
@@ -1,467 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.row;
-
-import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
-
-import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP;
-
-import static junit.framework.Assert.assertNotNull;
-
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.app.Notification;
-import android.content.Context;
-import android.content.pm.LauncherApps;
-import android.os.Handler;
-import android.service.notification.NotificationListenerService;
-import android.service.notification.NotificationListenerService.Ranking;
-import android.service.notification.StatusBarNotification;
-import android.testing.AndroidTestingRunner;
-import android.testing.TestableLooper;
-
-import androidx.asynclayoutinflater.view.AsyncLayoutInflater;
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.statusbar.IStatusBarService;
-import com.android.internal.util.NotificationMessagingUtil;
-import com.android.systemui.R;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.classifier.FalsingCollectorFake;
-import com.android.systemui.classifier.FalsingManagerFake;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
-import com.android.systemui.media.MediaFeatureFlag;
-import com.android.systemui.media.dialog.MediaOutputDialogFactory;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shared.plugins.PluginManager;
-import com.android.systemui.statusbar.NotificationListener;
-import com.android.systemui.statusbar.NotificationLockscreenUserManager;
-import com.android.systemui.statusbar.NotificationMediaManager;
-import com.android.systemui.statusbar.NotificationPresenter;
-import com.android.systemui.statusbar.NotificationRemoteInputManager;
-import com.android.systemui.statusbar.SbnBuilder;
-import com.android.systemui.statusbar.SmartReplyController;
-import com.android.systemui.statusbar.notification.ConversationNotificationProcessor;
-import com.android.systemui.statusbar.notification.NotifPipelineFlags;
-import com.android.systemui.statusbar.notification.NotificationClicker;
-import com.android.systemui.statusbar.notification.NotificationEntryListener;
-import com.android.systemui.statusbar.notification.NotificationEntryManager;
-import com.android.systemui.statusbar.notification.NotificationEntryManagerLogger;
-import com.android.systemui.statusbar.notification.NotificationFilter;
-import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
-import com.android.systemui.statusbar.notification.collection.NotifLiveDataStoreMocksKt;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationRankingManager;
-import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
-import com.android.systemui.statusbar.notification.collection.legacy.LowPriorityInflationHelper;
-import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
-import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider;
-import com.android.systemui.statusbar.notification.icon.IconBuilder;
-import com.android.systemui.statusbar.notification.icon.IconManager;
-import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProvider;
-import com.android.systemui.statusbar.notification.logging.NotificationLogger;
-import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier;
-import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent;
-import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent;
-import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
-import com.android.systemui.statusbar.phone.KeyguardBypassController;
-import com.android.systemui.statusbar.policy.HeadsUpManager;
-import com.android.systemui.statusbar.policy.InflatedSmartReplyState;
-import com.android.systemui.statusbar.policy.InflatedSmartReplyViewHolder;
-import com.android.systemui.statusbar.policy.SmartReplyConstants;
-import com.android.systemui.statusbar.policy.SmartReplyStateInflater;
-import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.leak.LeakDetector;
-import com.android.systemui.util.time.FakeSystemClock;
-import com.android.systemui.wmshell.BubblesManager;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Answers;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-import org.mockito.stubbing.Answer;
-
-import java.util.Optional;
-import java.util.concurrent.CountDownLatch;
-
-/**
- * Functional tests for notification inflation from {@link NotificationEntryManager}.
- */
-@SmallTest
-@RunWith(AndroidTestingRunner.class)
-@TestableLooper.RunWithLooper(setAsMainLooper = true)
-public class NotificationEntryManagerInflationTest extends SysuiTestCase {
-
-    private static final String TEST_TITLE = "Title";
-    private static final String TEST_TEXT = "Text";
-    private static final long TIMEOUT_TIME = 10000;
-    private static final Runnable TIMEOUT_RUNNABLE = () -> {
-        throw new RuntimeException("Timed out waiting to inflate");
-    };
-
-    @Mock private NotificationListener mNotificationListener;
-    @Mock private NotificationPresenter mPresenter;
-    @Mock private NotificationEntryManager.KeyguardEnvironment mEnvironment;
-    @Mock private NotificationListContainer mListContainer;
-    @Mock private NotificationEntryListener mEntryListener;
-    @Mock private NotificationRowBinderImpl.BindRowCallback mBindCallback;
-    @Mock private HeadsUpManager mHeadsUpManager;
-    @Mock private NotificationInterruptStateProvider mNotificationInterruptionStateProvider;
-    @Mock private NotificationLockscreenUserManager mLockscreenUserManager;
-    @Mock private NotificationGutsManager mGutsManager;
-    @Mock private NotificationRemoteInputManager mRemoteInputManager;
-    @Mock private NotificationMediaManager mNotificationMediaManager;
-    @Mock(answer = Answers.RETURNS_SELF)
-    private ExpandableNotificationRowComponent.Builder mExpandableNotificationRowComponentBuilder;
-    @Mock private ExpandableNotificationRowComponent mExpandableNotificationRowComponent;
-    @Mock private KeyguardBypassController mKeyguardBypassController;
-    @Mock private StatusBarStateController mStatusBarStateController;
-
-    @Mock private NotificationGroupManagerLegacy mGroupMembershipManager;
-    @Mock private NotificationGroupManagerLegacy mGroupExpansionManager;
-    @Mock private NotifPipelineFlags mNotifPipelineFlags;
-    @Mock private LeakDetector mLeakDetector;
-
-    @Mock private ActivatableNotificationViewController mActivatableNotificationViewController;
-    @Mock private NotificationRowComponent.Builder mNotificationRowComponentBuilder;
-    @Mock private PeopleNotificationIdentifier mPeopleNotificationIdentifier;
-    @Mock private InflatedSmartReplyState mInflatedSmartReplyState;
-    @Mock private InflatedSmartReplyViewHolder mInflatedSmartReplies;
-
-    private StatusBarNotification mSbn;
-    private NotificationListenerService.RankingMap mRankingMap;
-    private NotificationEntryManager mEntryManager;
-    private NotificationRowBinderImpl mRowBinder;
-    private Handler mHandler;
-    private FakeExecutor mBgExecutor;
-    private RowContentBindStage mRowContentBindStage;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        mDependency.injectMockDependency(SmartReplyController.class);
-        mDependency.injectMockDependency(MediaOutputDialogFactory.class);
-
-        mHandler = Handler.createAsync(TestableLooper.get(this).getLooper());
-
-        // Add an action so heads up content views are made
-        Notification.Action action = new Notification.Action.Builder(null, null, null).build();
-        Notification notification = new Notification.Builder(mContext)
-                .setSmallIcon(R.drawable.ic_person)
-                .setContentTitle(TEST_TITLE)
-                .setContentText(TEST_TEXT)
-                .setActions(action)
-                .build();
-        mSbn = new SbnBuilder()
-                .setNotification(notification)
-                .build();
-
-        when(mNotifPipelineFlags.checkLegacyPipelineEnabled()).thenReturn(true);
-        when(mNotifPipelineFlags.isNewPipelineEnabled()).thenReturn(false);
-
-        mEntryManager = new NotificationEntryManager(
-                mock(NotificationEntryManagerLogger.class),
-                mGroupMembershipManager,
-                mNotifPipelineFlags,
-                () -> mRowBinder,
-                () -> mRemoteInputManager,
-                mLeakDetector,
-                mock(IStatusBarService.class),
-                NotifLiveDataStoreMocksKt.createNotifLiveDataStoreImplMock(),
-                mock(DumpManager.class),
-                mBgExecutor
-        );
-        mEntryManager.initialize(
-                mNotificationListener,
-                new NotificationRankingManager(
-                        () -> mock(NotificationMediaManager.class),
-                        mGroupMembershipManager,
-                        mHeadsUpManager,
-                        mock(NotificationFilter.class),
-                        mock(NotificationEntryManagerLogger.class),
-                        mock(NotificationSectionsFeatureManager.class),
-                        mock(PeopleNotificationIdentifier.class),
-                        mock(HighPriorityProvider.class),
-                        mEnvironment));
-
-        NotifRemoteViewCache cache = new NotifRemoteViewCacheImpl(mEntryManager);
-        NotifBindPipeline pipeline = new NotifBindPipeline(
-                mEntryManager,
-                mock(NotifBindPipelineLogger.class),
-                TestableLooper.get(this).getLooper());
-        mBgExecutor = new FakeExecutor(new FakeSystemClock());
-        NotificationContentInflater binder = new NotificationContentInflater(
-                cache,
-                mRemoteInputManager,
-                mock(ConversationNotificationProcessor.class),
-                mock(MediaFeatureFlag.class),
-                mBgExecutor,
-                new SmartReplyStateInflater() {
-                    @Override
-                    public InflatedSmartReplyState inflateSmartReplyState(NotificationEntry entry) {
-                        return mInflatedSmartReplyState;
-                    }
-
-                    @Override
-                    public InflatedSmartReplyViewHolder inflateSmartReplyViewHolder(
-                            Context sysuiContext, Context notifPackageContext,
-                            NotificationEntry entry,
-                            InflatedSmartReplyState existingSmartReplyState,
-                            InflatedSmartReplyState newSmartReplyState) {
-                        return mInflatedSmartReplies;
-                    }
-                });
-        mRowContentBindStage = new RowContentBindStage(
-                binder,
-                mock(NotifInflationErrorManager.class),
-                mock(RowContentBindStageLogger.class));
-        pipeline.setStage(mRowContentBindStage);
-
-        ArgumentCaptor<ExpandableNotificationRow> viewCaptor =
-                ArgumentCaptor.forClass(ExpandableNotificationRow.class);
-        when(mExpandableNotificationRowComponentBuilder
-                .expandableNotificationRow(viewCaptor.capture()))
-                .thenReturn(mExpandableNotificationRowComponentBuilder);
-        when(mExpandableNotificationRowComponentBuilder.build())
-                .thenReturn(mExpandableNotificationRowComponent);
-
-        when(mExpandableNotificationRowComponent.getExpandableNotificationRowController())
-                .thenAnswer((Answer<ExpandableNotificationRowController>) invocation ->
-                        new ExpandableNotificationRowController(
-                                viewCaptor.getValue(),
-                                mock(ActivatableNotificationViewController.class),
-                                mock(RemoteInputViewSubcomponent.Factory.class),
-                                mock(MetricsLogger.class),
-                                mListContainer,
-                                mNotificationMediaManager,
-                                mock(SmartReplyConstants.class),
-                                mock(SmartReplyController.class),
-                                mock(PluginManager.class),
-                                new FakeSystemClock(),
-                                "FOOBAR",
-                                "FOOBAR",
-                                mKeyguardBypassController,
-                                mGroupMembershipManager,
-                                mGroupExpansionManager,
-                                mRowContentBindStage,
-                                mock(NotificationLogger.class),
-                                mHeadsUpManager,
-                                mPresenter,
-                                mStatusBarStateController,
-                                mGutsManager,
-                                true,
-                                null,
-                                new FalsingManagerFake(),
-                                new FalsingCollectorFake(),
-                                mock(FeatureFlags.class),
-                                mPeopleNotificationIdentifier,
-                                Optional.of(mock(BubblesManager.class)),
-                                mock(ExpandableNotificationRowDragController.class)));
-
-        when(mNotificationRowComponentBuilder.activatableNotificationView(any()))
-                .thenReturn(mNotificationRowComponentBuilder);
-        when(mNotificationRowComponentBuilder.build()).thenReturn(
-                () -> mActivatableNotificationViewController);
-
-        mRowBinder = new NotificationRowBinderImpl(
-                mContext,
-                new NotificationMessagingUtil(mContext),
-                mRemoteInputManager,
-                mLockscreenUserManager,
-                pipeline,
-                mRowContentBindStage,
-                RowInflaterTask::new,
-                mExpandableNotificationRowComponentBuilder,
-                new IconManager(
-                        mEntryManager,
-                        mock(LauncherApps.class),
-                        new IconBuilder(mContext)),
-                mock(LowPriorityInflationHelper.class),
-                mNotifPipelineFlags);
-
-        mEntryManager.setUpWithPresenter(mPresenter);
-        mEntryManager.addNotificationEntryListener(mEntryListener);
-
-        mRowBinder.setUpWithPresenter(mPresenter, mListContainer, mBindCallback);
-        mRowBinder.setNotificationClicker(mock(NotificationClicker.class));
-
-        Ranking ranking = new Ranking();
-        ranking.populate(
-                mSbn.getKey(),
-                0,
-                false,
-                0,
-                0,
-                IMPORTANCE_DEFAULT,
-                null,
-                null,
-                null,
-                null,
-                null,
-                true,
-                Ranking.USER_SENTIMENT_NEUTRAL,
-                false,
-                -1,
-                false,
-                null,
-                null,
-                false,
-                false,
-                false,
-                null,
-                0,
-                false
-            );
-        mRankingMap = new NotificationListenerService.RankingMap(new Ranking[] {ranking});
-
-        TestableLooper.get(this).processAllMessages();
-    }
-
-    @After
-    public void cleanUp() {
-        // Don't leave anything on main thread
-        TestableLooper.get(this).processAllMessages();
-    }
-
-    @Test
-    public void testAddNotification() {
-        // WHEN a notification is added
-        mEntryManager.addNotification(mSbn, mRankingMap);
-        ArgumentCaptor<NotificationEntry> entryCaptor = ArgumentCaptor.forClass(
-                NotificationEntry.class);
-        verify(mEntryListener).onPendingEntryAdded(entryCaptor.capture());
-        NotificationEntry entry = entryCaptor.getValue();
-
-        waitForInflation();
-
-        // THEN the notification has its row inflated
-        assertNotNull(entry.getRow());
-        assertNotNull(entry.getRow().getPrivateLayout().getContractedChild());
-
-        // THEN inflation callbacks are called
-        verify(mBindCallback).onBindRow(entry.getRow());
-        verify(mEntryListener, never()).onInflationError(any(), any());
-        verify(mEntryListener).onEntryInflated(entry);
-        verify(mEntryListener).onNotificationAdded(entry);
-
-        // THEN the notification is active
-        assertNotNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey()));
-
-        // THEN we update the presenter
-        verify(mPresenter).updateNotificationViews(any());
-    }
-
-    @Test
-    public void testUpdateNotification() {
-        // GIVEN a notification already added
-        mEntryManager.addNotification(mSbn, mRankingMap);
-        ArgumentCaptor<NotificationEntry> entryCaptor = ArgumentCaptor.forClass(
-                NotificationEntry.class);
-        verify(mEntryListener).onPendingEntryAdded(entryCaptor.capture());
-        NotificationEntry entry = entryCaptor.getValue();
-        waitForInflation();
-
-        Mockito.reset(mEntryListener);
-        Mockito.reset(mPresenter);
-
-        // WHEN the notification is updated
-        mEntryManager.updateNotification(mSbn, mRankingMap);
-
-        waitForInflation();
-
-        // THEN the notification has its row and inflated
-        assertNotNull(entry.getRow());
-
-        // THEN inflation callbacks are called
-        verify(mEntryListener, never()).onInflationError(any(), any());
-        verify(mEntryListener).onEntryReinflated(entry);
-
-        // THEN we update the presenter
-        verify(mPresenter).updateNotificationViews(any());
-    }
-
-    @Test
-    public void testContentViewInflationDuringRowInflationInflatesCorrectViews() {
-        // GIVEN a notification is added and the row is inflating
-        mEntryManager.addNotification(mSbn, mRankingMap);
-        ArgumentCaptor<NotificationEntry> entryCaptor = ArgumentCaptor.forClass(
-                NotificationEntry.class);
-        verify(mEntryListener).onPendingEntryAdded(entryCaptor.capture());
-        NotificationEntry entry = entryCaptor.getValue();
-
-        // WHEN we try to bind a content view
-        mRowContentBindStage.getStageParams(entry).requireContentViews(FLAG_CONTENT_VIEW_HEADS_UP);
-        mRowContentBindStage.requestRebind(entry, null);
-
-        waitForInflation();
-
-        // THEN the notification has its row and all relevant content views inflated
-        assertNotNull(entry.getRow());
-        assertNotNull(entry.getRow().getPrivateLayout().getContractedChild());
-        assertNotNull(entry.getRow().getPrivateLayout().getHeadsUpChild());
-    }
-
-    /**
-     * Wait for inflation to finish.
-     *
-     * A few things to note
-     * 1) Row inflation is done via {@link AsyncLayoutInflater} on its own background thread that
-     * calls back to main thread which is why we wait on main thread.
-     * 2) Row *content* inflation is done on the {@link FakeExecutor} we pass in in this test class
-     * so we control when that work is done. The callback is still always on the main thread.
-     */
-    private void waitForInflation() {
-        mHandler.postDelayed(TIMEOUT_RUNNABLE, TIMEOUT_TIME);
-        final CountDownLatch latch = new CountDownLatch(1);
-        NotificationEntryListener inflationListener = new NotificationEntryListener() {
-            @Override
-            public void onEntryInflated(NotificationEntry entry) {
-                latch.countDown();
-            }
-
-            @Override
-            public void onEntryReinflated(NotificationEntry entry) {
-                latch.countDown();
-            }
-
-            @Override
-            public void onInflationError(StatusBarNotification notification, Exception exception) {
-                latch.countDown();
-            }
-        };
-        mEntryManager.addNotificationEntryListener(inflationListener);
-        while (latch.getCount() != 0) {
-            mBgExecutor.runAllReady();
-            TestableLooper.get(this).processMessages(1);
-        }
-        mHandler.removeCallbacks(TIMEOUT_RUNNABLE);
-        mEntryManager.removeNotificationEntryListener(inflationListener);
-    }
-
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
index e26c583..f57c409 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java
@@ -156,13 +156,13 @@
 
         mGutsManager = new NotificationGutsManager(mContext,
                 () -> Optional.of(mCentralSurfaces), mHandler, mHandler, mAccessibilityManager,
-                mHighPriorityProvider, mINotificationManager, mNotificationEntryManager,
+                mHighPriorityProvider, mINotificationManager,
                 mPeopleSpaceWidgetManager, mLauncherApps, mShortcutManager,
                 mChannelEditorDialogController, mContextTracker, mAssistantFeedbackController,
                 Optional.of(mBubblesManager), new UiEventLoggerFake(), mOnUserInteractionCallback,
                 mShadeController, mock(DumpManager.class));
         mGutsManager.setUpWithPresenter(mPresenter, mNotificationListContainer,
-                mCheckSaveListener, mOnSettingsClickListener);
+                mOnSettingsClickListener);
         mGutsManager.setNotificationActivityStarter(mNotificationActivityStarter);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 30c40b9..2a3509c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -121,7 +121,6 @@
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.OperatorNameViewController;
 import com.android.systemui.statusbar.PulseExpansionHandler;
 import com.android.systemui.statusbar.StatusBarState;
@@ -129,7 +128,6 @@
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
-import com.android.systemui.statusbar.notification.NotificationFilter;
 import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
 import com.android.systemui.statusbar.notification.collection.NotifLiveDataStore;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
@@ -220,7 +218,6 @@
     @Mock private BatteryController mBatteryController;
     @Mock private DeviceProvisionedController mDeviceProvisionedController;
     @Mock private StatusBarNotificationPresenter mNotificationPresenter;
-    @Mock private NotificationFilter mNotificationFilter;
     @Mock private AmbientDisplayConfiguration mAmbientDisplayConfiguration;
     @Mock private NotificationLogger.ExpansionStateLogger mExpansionStateLogger;
     @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@@ -242,7 +239,6 @@
     @Mock private AutoHideController mAutoHideController;
     @Mock private StatusBarWindowController mStatusBarWindowController;
     @Mock private StatusBarWindowStateController mStatusBarWindowStateController;
-    @Mock private NotificationViewHierarchyManager mNotificationViewHierarchyManager;
     @Mock private UserSwitcherController mUserSwitcherController;
     @Mock private Bubbles mBubbles;
     @Mock private NotificationShadeWindowController mNotificationShadeWindowController;
@@ -283,7 +279,6 @@
     @Mock private OperatorNameViewController mOperatorNameViewController;
     @Mock private OperatorNameViewController.Factory mOperatorNameViewControllerFactory;
     @Mock private ActivityLaunchAnimator mActivityLaunchAnimator;
-    @Mock private NotifPipelineFlags mNotifPipelineFlags;
     @Mock private NotifLiveDataStore mNotifLiveDataStore;
     @Mock private InteractionJankMonitor mJankMonitor;
     @Mock private DeviceStateManager mDeviceStateManager;
@@ -298,7 +293,6 @@
     @Before
     public void setup() throws Exception {
         MockitoAnnotations.initMocks(this);
-        mDependency.injectTestDependency(NotificationFilter.class, mNotificationFilter);
 
         IPowerManager powerManagerService = mock(IPowerManager.class);
         IThermalService thermalService = mock(IThermalService.class);
@@ -310,7 +304,6 @@
                         mPowerManager,
                         mDreamManager,
                         mAmbientDisplayConfiguration,
-                        mNotificationFilter,
                         mStatusBarStateController,
                         mKeyguardStateController,
                         mBatteryController,
@@ -410,7 +403,6 @@
                 mNotificationGutsManager,
                 notificationLogger,
                 mNotificationInterruptStateProvider,
-                mNotificationViewHierarchyManager,
                 new PanelExpansionStateManager(),
                 mKeyguardViewMediator,
                 new DisplayMetrics(),
@@ -472,7 +464,6 @@
                 mWallpaperManager,
                 Optional.of(mStartingSurface),
                 mActivityLaunchAnimator,
-                mNotifPipelineFlags,
                 mJankMonitor,
                 mDeviceStateManager,
                 mWiredChargingRippleController, mDreamManager);
@@ -659,7 +650,6 @@
     public void testShouldHeadsUp_nonSuppressedGroupSummary() throws Exception {
         when(mPowerManager.isScreenOn()).thenReturn(true);
         when(mHeadsUpManager.isSnoozed(anyString())).thenReturn(false);
-        when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false);
         when(mDreamManager.isDreaming()).thenReturn(false);
 
         Notification n = new Notification.Builder(getContext(), "a")
@@ -683,7 +673,6 @@
     public void testShouldHeadsUp_suppressedGroupSummary() throws Exception {
         when(mPowerManager.isScreenOn()).thenReturn(true);
         when(mHeadsUpManager.isSnoozed(anyString())).thenReturn(false);
-        when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false);
         when(mDreamManager.isDreaming()).thenReturn(false);
 
         Notification n = new Notification.Builder(getContext(), "a")
@@ -707,7 +696,6 @@
     public void testShouldHeadsUp_suppressedHeadsUp() throws Exception {
         when(mPowerManager.isScreenOn()).thenReturn(true);
         when(mHeadsUpManager.isSnoozed(anyString())).thenReturn(false);
-        when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false);
         when(mDreamManager.isDreaming()).thenReturn(false);
 
         Notification n = new Notification.Builder(getContext(), "a").build();
@@ -729,7 +717,6 @@
     public void testShouldHeadsUp_noSuppressedHeadsUp() throws Exception {
         when(mPowerManager.isScreenOn()).thenReturn(true);
         when(mHeadsUpManager.isSnoozed(anyString())).thenReturn(false);
-        when(mNotificationFilter.shouldFilterOut(any())).thenReturn(false);
         when(mDreamManager.isDreaming()).thenReturn(false);
 
         Notification n = new Notification.Builder(getContext(), "a").build();
@@ -1041,7 +1028,6 @@
                 PowerManager powerManager,
                 IDreamManager dreamManager,
                 AmbientDisplayConfiguration ambientDisplayConfiguration,
-                NotificationFilter filter,
                 StatusBarStateController controller,
                 KeyguardStateController keyguardStateController,
                 BatteryController batteryController,
@@ -1055,7 +1041,6 @@
                     powerManager,
                     dreamManager,
                     ambientDisplayConfiguration,
-                    filter,
                     batteryController,
                     controller,
                     keyguardStateController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
index 6540135..c0243dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxAppearanceCalculatorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.phone
 
+import android.graphics.Color
 import android.graphics.Rect
 import android.testing.AndroidTestingRunner
 import android.view.WindowInsetsController
@@ -24,6 +25,7 @@
 import com.android.internal.statusbar.LetterboxDetails
 import com.android.internal.view.AppearanceRegion
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent
 import com.google.common.truth.Expect
 import com.google.common.truth.Truth.assertThat
@@ -33,7 +35,6 @@
 import org.junit.runner.RunWith
 import org.mockito.Mock
 import org.mockito.Mockito.`when` as whenever
-import com.android.systemui.dump.DumpManager
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
@@ -56,6 +57,7 @@
     @Mock private lateinit var statusBarBoundsProvider: StatusBarBoundsProvider
     @Mock private lateinit var statusBarFragmentComponent: StatusBarFragmentComponent
     @Mock private lateinit var dumpManager: DumpManager
+    @Mock private lateinit var letterboxBackgroundProvider: LetterboxBackgroundProvider
 
     private lateinit var calculator: LetterboxAppearanceCalculator
 
@@ -63,8 +65,12 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(statusBarFragmentComponent.boundsProvider).thenReturn(statusBarBoundsProvider)
-        calculator = LetterboxAppearanceCalculator(lightBarController, dumpManager)
+        calculator =
+            LetterboxAppearanceCalculator(
+                lightBarController, dumpManager, letterboxBackgroundProvider)
         calculator.onStatusBarViewInitialized(statusBarFragmentComponent)
+        whenever(letterboxBackgroundProvider.letterboxBackgroundColor).thenReturn(Color.BLACK)
+        whenever(letterboxBackgroundProvider.isLetterboxBackgroundMultiColored).thenReturn(false)
     }
 
     @Test
@@ -100,6 +106,23 @@
     }
 
     @Test
+    fun getLetterboxAppearance_noOverlap_BackgroundMultiColor_returnsAppearanceWithScrim() {
+        whenever(letterboxBackgroundProvider.isLetterboxBackgroundMultiColored).thenReturn(true)
+        whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
+        whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
+        val letterbox = letterboxWithInnerBounds(Rect(101, 0, 199, 100))
+
+        val letterboxAppearance =
+            calculator.getLetterboxAppearance(
+                TEST_APPEARANCE, TEST_APPEARANCE_REGIONS, arrayOf(letterbox))
+
+        expect
+                .that(letterboxAppearance.appearance)
+                .isEqualTo(TEST_APPEARANCE or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS)
+        expect.that(letterboxAppearance.appearanceRegions).isEqualTo(TEST_APPEARANCE_REGIONS)
+    }
+
+    @Test
     fun getLetterboxAppearance_noOverlap_returnsAppearanceWithoutScrim() {
         whenever(statusBarBoundsProvider.visibleStartSideBounds).thenReturn(Rect(0, 0, 100, 100))
         whenever(statusBarBoundsProvider.visibleEndSideBounds).thenReturn(Rect(200, 0, 300, 100))
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
new file mode 100644
index 0000000..44325dd
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LetterboxBackgroundProviderTest.kt
@@ -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 com.android.systemui.statusbar.phone
+
+import android.graphics.Color
+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.time.FakeSystemClock
+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.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LetterboxBackgroundProviderTest : SysuiTestCase() {
+
+    private val fakeSystemClock = FakeSystemClock()
+    private val fakeExecutor = FakeExecutor(fakeSystemClock)
+
+    @Mock private lateinit var windowManager: IWindowManager
+    @Mock private lateinit var dumpManager: DumpManager
+
+    private lateinit var provider: LetterboxBackgroundProvider
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+
+        provider = LetterboxBackgroundProvider(windowManager, fakeExecutor, dumpManager)
+    }
+
+    @Test
+    fun letterboxBackgroundColor_defaultValue_returnsBlack() {
+        assertThat(provider.letterboxBackgroundColor).isEqualTo(Color.BLACK)
+    }
+
+    @Test
+    fun letterboxBackgroundColor_afterOnStart_executorNotDone_returnsDefaultValue() {
+        whenever(windowManager.letterboxBackgroundColorInArgb).thenReturn(Color.RED)
+
+        provider.start()
+
+        assertThat(provider.letterboxBackgroundColor).isEqualTo(Color.BLACK)
+    }
+
+    @Test
+    fun letterboxBackgroundColor_afterOnStart_executorDone_returnsValueFromWindowManager() {
+        whenever(windowManager.letterboxBackgroundColorInArgb).thenReturn(Color.RED)
+
+        provider.start()
+        fakeExecutor.runAllReady()
+
+        assertThat(provider.letterboxBackgroundColor).isEqualTo(Color.RED)
+    }
+
+    @Test
+    fun isLetterboxBackgroundMultiColored_defaultValue_returnsFalse() {
+        assertThat(provider.isLetterboxBackgroundMultiColored).isEqualTo(false)
+    }
+    @Test
+    fun isLetterboxBackgroundMultiColored_afterOnStart_executorNotDone_returnsDefaultValue() {
+        whenever(windowManager.isLetterboxBackgroundMultiColored).thenReturn(true)
+
+        provider.start()
+
+        assertThat(provider.isLetterboxBackgroundMultiColored).isFalse()
+    }
+
+    @Test
+    fun isBackgroundMultiColored_afterOnStart_executorDone_returnsValueFromWindowManager() {
+        whenever(windowManager.isLetterboxBackgroundMultiColored).thenReturn(true)
+
+        provider.start()
+        fakeExecutor.runAllReady()
+
+        assertThat(provider.isLetterboxBackgroundMultiColored).isTrue()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
index 43c6fe1..eef43bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java
@@ -43,7 +43,6 @@
 import com.android.systemui.shade.NotificationPanelViewController;
 import com.android.systemui.shade.NotificationShadeWindowView;
 import com.android.systemui.shade.ShadeController;
-import com.android.systemui.shade.ShadeControllerImpl;
 import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.KeyguardIndicationController;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
@@ -51,7 +50,6 @@
 import com.android.systemui.statusbar.NotificationMediaManager;
 import com.android.systemui.statusbar.NotificationRemoteInputManager;
 import com.android.systemui.statusbar.NotificationShadeWindowController;
-import com.android.systemui.statusbar.NotificationViewHierarchyManager;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
@@ -77,16 +75,17 @@
 @RunWithLooper()
 public class StatusBarNotificationPresenterTest extends SysuiTestCase {
     private StatusBarNotificationPresenter mStatusBarNotificationPresenter;
-    private NotificationInterruptStateProvider mNotificationInterruptStateProvider =
+    private final NotificationInterruptStateProvider mNotificationInterruptStateProvider =
             mock(NotificationInterruptStateProvider.class);
     private NotificationInterruptSuppressor mInterruptSuppressor;
     private CommandQueue mCommandQueue;
     private FakeMetricsLogger mMetricsLogger;
-    private ShadeController mShadeController = mock(ShadeController.class);
-    private CentralSurfaces mCentralSurfaces = mock(CentralSurfaces.class);
-    private KeyguardStateController mKeyguardStateController = mock(KeyguardStateController.class);
-    private NotifPipelineFlags mNotifPipelineFlags = mock(NotifPipelineFlags.class);
-    private InitController mInitController = new InitController();
+    private final ShadeController mShadeController = mock(ShadeController.class);
+    private final CentralSurfaces mCentralSurfaces = mock(CentralSurfaces.class);
+    private final KeyguardStateController mKeyguardStateController =
+            mock(KeyguardStateController.class);
+    private final NotifPipelineFlags mNotifPipelineFlags = mock(NotifPipelineFlags.class);
+    private final InitController mInitController = new InitController();
 
     @Before
     public void setup() {
@@ -117,16 +116,13 @@
                 mock(ActivityStarter.class),
                 stackScrollLayoutController,
                 mock(DozeScrimController.class),
-                mock(ScrimController.class),
                 mock(NotificationShadeWindowController.class),
                 mock(DynamicPrivacyController.class),
                 mKeyguardStateController,
                 mock(KeyguardIndicationController.class),
                 mCentralSurfaces,
-                mock(ShadeControllerImpl.class),
                 mock(LockscreenShadeTransitionController.class),
                 mCommandQueue,
-                mock(NotificationViewHierarchyManager.class),
                 mock(NotificationLockscreenUserManager.class),
                 mock(SysuiStatusBarStateController.class),
                 mock(NotifShadeEventSource.class),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
index 56804d5..25348f3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/fragment/CollapsedStatusBarFragmentTest.java
@@ -230,10 +230,9 @@
     }
 
     @Test
-    public void disable_isDozingButNoCustomClock_clockAndSystemInfoVisible() {
+    public void disable_isDozing_clockAndSystemInfoVisible() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         when(mStatusBarStateController.isDozing()).thenReturn(true);
-        when(mNotificationPanelViewController.hasCustomClock()).thenReturn(false);
 
         fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
 
@@ -242,10 +241,9 @@
     }
 
     @Test
-    public void disable_customClockButNotDozing_clockAndSystemInfoVisible() {
+    public void disable_NotDozing_clockAndSystemInfoVisible() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mNotificationPanelViewController.hasCustomClock()).thenReturn(true);
 
         fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
 
@@ -254,40 +252,6 @@
     }
 
     @Test
-    public void disable_dozingAndCustomClock_clockAndSystemInfoHidden() {
-        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-        when(mStatusBarStateController.isDozing()).thenReturn(true);
-        when(mNotificationPanelViewController.hasCustomClock()).thenReturn(true);
-
-        // Make sure they start out as visible
-        assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
-        assertEquals(View.VISIBLE, getClockView().getVisibility());
-
-        fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
-
-        assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
-        assertEquals(View.GONE, getClockView().getVisibility());
-    }
-
-    @Test
-    public void onDozingChanged_clockAndSystemInfoVisibilitiesUpdated() {
-        CollapsedStatusBarFragment fragment = resumeAndGetFragment();
-        when(mStatusBarStateController.isDozing()).thenReturn(true);
-        when(mNotificationPanelViewController.hasCustomClock()).thenReturn(true);
-
-        // Make sure they start out as visible
-        assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
-        assertEquals(View.VISIBLE, getClockView().getVisibility());
-
-        fragment.onDozingChanged(true);
-
-        // When this callback is triggered, we want to make sure the clock and system info
-        // visibilities are recalculated. Since dozing=true, they shouldn't be visible.
-        assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
-        assertEquals(View.GONE, getClockView().getVisibility());
-    }
-
-    @Test
     public void disable_headsUpShouldBeVisibleTrue_clockDisabled() {
         CollapsedStatusBarFragment fragment = resumeAndGetFragment();
         when(mHeadsUpAppearanceController.shouldBeVisible()).thenReturn(true);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLoggerTest.kt
new file mode 100644
index 0000000..2915ae8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/ConnectivityPipelineLoggerTest.kt
@@ -0,0 +1,75 @@
+/*
+ * 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.statusbar.pipeline
+
+import android.net.Network
+import android.net.NetworkCapabilities
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.log.LogcatEchoTracker
+import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.mock
+
+@SmallTest
+class ConnectivityPipelineLoggerTest : SysuiTestCase() {
+    private val buffer = LogBufferFactory(DumpManager(), mock(LogcatEchoTracker::class.java))
+        .create("buffer", 10)
+    private val logger = ConnectivityPipelineLogger(buffer)
+
+    @Test
+    fun testLogNetworkCapsChange_bufferHasInfo() {
+        logger.logOnCapabilitiesChanged(NET_1, NET_1_CAPS)
+
+        val stringWriter = StringWriter()
+        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
+        val actualString = stringWriter.toString()
+
+        val expectedNetId = NET_1_ID.toString()
+        val expectedCaps = NET_1_CAPS.toString()
+
+        assertThat(actualString).contains(expectedNetId)
+        assertThat(actualString).contains(expectedCaps)
+    }
+
+    @Test
+    fun testLogOnLost_bufferHasNetIdOfLostNetwork() {
+        logger.logOnLost(NET_1)
+
+        val stringWriter = StringWriter()
+        buffer.dump(PrintWriter(stringWriter), tailLength = 0)
+        val actualString = stringWriter.toString()
+
+        val expectedNetId = NET_1_ID.toString()
+
+        assertThat(actualString).contains(expectedNetId)
+    }
+
+    private val NET_1_ID = 100
+    private val NET_1 = com.android.systemui.util.mockito.mock<Network>().also {
+        Mockito.`when`(it.getNetId()).thenReturn(NET_1_ID)
+    }
+    private val NET_1_CAPS = NetworkCapabilities.Builder()
+        .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
+        .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
+        .build()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepoTest.kt
new file mode 100644
index 0000000..40f8fbf
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/repository/NetworkCapabilitiesRepoTest.kt
@@ -0,0 +1,252 @@
+/*
+ * 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.statusbar.pipeline.repository
+
+import android.net.ConnectivityManager
+import android.net.ConnectivityManager.NetworkCallback
+import android.net.Network
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED
+import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.NetworkRequest
+import android.test.suitebuilder.annotation.SmallTest
+import android.testing.AndroidTestingRunner
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.pipeline.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when` as whenever
+import org.mockito.MockitoAnnotations
+
+// TODO(b/240619365): Update this test to use `runTest` when we update the testing library
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+class NetworkCapabilitiesRepoTest : SysuiTestCase() {
+    @Mock private lateinit var connectivityManager: ConnectivityManager
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+    }
+
+    @Test
+    fun testOnCapabilitiesChanged_oneNewNetwork_networkStored() = runBlocking {
+        // GIVEN a repo hooked up to [ConnectivityManager]
+        val scope = CoroutineScope(Dispatchers.Unconfined)
+        val repo = NetworkCapabilitiesRepo(
+            connectivityManager = connectivityManager,
+            scope = scope,
+            logger = logger,
+        )
+
+        val job = launch(start = CoroutineStart.UNDISPATCHED) {
+            repo.dataStream.collect {
+            }
+        }
+
+        val callback: NetworkCallback = withArgCaptor {
+            verify(connectivityManager)
+                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
+        }
+
+        // WHEN a new network is added
+        callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
+
+        val currentMap = repo.dataStream.value
+
+        // THEN it is emitted from the flow
+        assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1)
+        assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS)
+
+        job.cancel()
+        scope.cancel()
+    }
+
+    @Test
+    fun testOnCapabilitiesChanged_twoNewNetworks_bothStored() = runBlocking {
+        // GIVEN a repo hooked up to [ConnectivityManager]
+        val scope = CoroutineScope(Dispatchers.Unconfined)
+        val repo = NetworkCapabilitiesRepo(
+            connectivityManager = connectivityManager,
+            scope = scope,
+            logger = logger,
+        )
+
+        val job = launch(start = CoroutineStart.UNDISPATCHED) {
+            repo.dataStream.collect {
+            }
+        }
+
+        val callback: NetworkCallback = withArgCaptor {
+            verify(connectivityManager)
+                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
+        }
+
+        // WHEN two new networks are added
+        callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
+        callback.onCapabilitiesChanged(NET_2, NET_2_CAPS)
+
+        val currentMap = repo.dataStream.value
+
+        // THEN the current state of the flow reflects 2 networks
+        assertThat(currentMap[NET_1_ID]?.network).isEqualTo(NET_1)
+        assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_1_CAPS)
+        assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2)
+        assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS)
+
+        job.cancel()
+        scope.cancel()
+    }
+
+    @Test
+    fun testOnCapabilitesChanged_newCapabilitiesForExistingNetwork_areCaptured() = runBlocking {
+        // GIVEN a repo hooked up to [ConnectivityManager]
+        val scope = CoroutineScope(Dispatchers.Unconfined)
+        val repo = NetworkCapabilitiesRepo(
+            connectivityManager = connectivityManager,
+            scope = scope,
+            logger = logger,
+        )
+
+        val job = launch(start = CoroutineStart.UNDISPATCHED) {
+            repo.dataStream.collect {
+            }
+        }
+
+        val callback: NetworkCallback = withArgCaptor {
+            verify(connectivityManager)
+                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
+        }
+
+        // WHEN a network is added, and then its capabilities are changed
+        callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
+        callback.onCapabilitiesChanged(NET_1, NET_2_CAPS)
+
+        val currentMap = repo.dataStream.value
+
+        // THEN the current state of the flow reflects the new capabilities
+        assertThat(currentMap[NET_1_ID]?.capabilities).isEqualTo(NET_2_CAPS)
+
+        job.cancel()
+        scope.cancel()
+    }
+
+    @Test
+    fun testOnLost_networkIsRemoved() = runBlocking {
+        // GIVEN a repo hooked up to [ConnectivityManager]
+        val scope = CoroutineScope(Dispatchers.Unconfined)
+        val repo = NetworkCapabilitiesRepo(
+            connectivityManager = connectivityManager,
+            scope = scope,
+            logger = logger,
+        )
+
+        val job = launch(start = CoroutineStart.UNDISPATCHED) {
+            repo.dataStream.collect {
+            }
+        }
+
+        val callback: NetworkCallback = withArgCaptor {
+            verify(connectivityManager)
+                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
+        }
+
+        // WHEN two new networks are added, and one is removed
+        callback.onCapabilitiesChanged(NET_1, NET_1_CAPS)
+        callback.onCapabilitiesChanged(NET_2, NET_2_CAPS)
+        callback.onLost(NET_1)
+
+        val currentMap = repo.dataStream.value
+
+        // THEN the current state of the flow reflects only the remaining network
+        assertThat(currentMap[NET_1_ID]).isNull()
+        assertThat(currentMap[NET_2_ID]?.network).isEqualTo(NET_2)
+        assertThat(currentMap[NET_2_ID]?.capabilities).isEqualTo(NET_2_CAPS)
+
+        job.cancel()
+        scope.cancel()
+    }
+
+    @Test
+    fun testOnLost_noNetworks_doesNotCrash() = runBlocking {
+        // GIVEN a repo hooked up to [ConnectivityManager]
+        val scope = CoroutineScope(Dispatchers.Unconfined)
+        val repo = NetworkCapabilitiesRepo(
+            connectivityManager = connectivityManager,
+            scope = scope,
+            logger = logger,
+        )
+
+        val job = launch(start = CoroutineStart.UNDISPATCHED) {
+            repo.dataStream.collect {
+            }
+        }
+
+        val callback: NetworkCallback = withArgCaptor {
+            verify(connectivityManager)
+                .registerNetworkCallback(any(NetworkRequest::class.java), capture())
+        }
+
+        // WHEN no networks are added, and one is removed
+        callback.onLost(NET_1)
+
+        val currentMap = repo.dataStream.value
+
+        // THEN the current state of the flow shows no networks
+        assertThat(currentMap).isEmpty()
+
+        job.cancel()
+        scope.cancel()
+    }
+
+    private val NET_1_ID = 100
+    private val NET_1 = mock<Network>().also {
+        whenever(it.getNetId()).thenReturn(NET_1_ID)
+    }
+    private val NET_2_ID = 200
+    private val NET_2 = mock<Network>().also {
+        whenever(it.getNetId()).thenReturn(NET_2_ID)
+    }
+
+    private val NET_1_CAPS = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_CELLULAR)
+        .addCapability(NET_CAPABILITY_VALIDATED)
+        .build()
+
+    private val NET_2_CAPS = NetworkCapabilities.Builder()
+        .addTransportType(TRANSPORT_WIFI)
+        .addCapability(NET_CAPABILITY_NOT_METERED)
+        .addCapability(NET_CAPABILITY_VALIDATED)
+        .build()
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 59a9a3c..fee17c7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -43,6 +43,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.ActivityManager;
 import android.app.IActivityManager;
 import android.app.INotificationManager;
 import android.app.Notification;
@@ -93,7 +94,6 @@
 import com.android.systemui.statusbar.RankingBuilder;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
-import com.android.systemui.statusbar.notification.NotificationFilter;
 import com.android.systemui.statusbar.notification.collection.NotifPipeline;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -134,6 +134,7 @@
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import org.junit.Before;
 import org.junit.Ignore;
@@ -143,7 +144,10 @@
 import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
+import org.mockito.stubbing.Answer;
 
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Optional;
@@ -215,6 +219,8 @@
     private BubbleEntry mBubbleEntry2User11;
 
     @Mock
+    private ShellInit mShellInit;
+    @Mock
     private ShellController mShellController;
     @Mock
     private Bubbles.BubbleExpandListener mBubbleExpandListener;
@@ -252,6 +258,8 @@
     private TaskViewTransitions mTaskViewTransitions;
     @Mock
     private Optional<OneHandedController> mOneHandedOptional;
+    @Mock
+    private UserManager mUserManager;
 
     private TestableBubblePositioner mPositioner;
 
@@ -314,12 +322,14 @@
         mPositioner.setMaxBubbles(5);
         mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, syncExecutor);
 
+        when(mUserManager.getProfiles(ActivityManager.getCurrentUser())).thenReturn(
+                Collections.singletonList(mock(UserInfo.class)));
+
         TestableNotificationInterruptStateProviderImpl interruptionStateProvider =
                 new TestableNotificationInterruptStateProviderImpl(mContext.getContentResolver(),
                         mock(PowerManager.class),
                         mock(IDreamManager.class),
                         mock(AmbientDisplayConfiguration.class),
-                        mock(NotificationFilter.class),
                         mock(StatusBarStateController.class),
                         mock(KeyguardStateController.class),
                         mock(BatteryController.class),
@@ -332,6 +342,7 @@
         when(mShellTaskOrganizer.getExecutor()).thenReturn(syncExecutor);
         mBubbleController = new TestableBubbleController(
                 mContext,
+                mShellInit,
                 mShellController,
                 mBubbleData,
                 mFloatingContentCoordinator,
@@ -339,7 +350,7 @@
                 mStatusBarService,
                 mWindowManager,
                 mWindowManagerShellWrapper,
-                mock(UserManager.class),
+                mUserManager,
                 mLauncherApps,
                 mBubbleLogger,
                 mTaskStackListener,
@@ -382,6 +393,11 @@
     }
 
     @Test
+    public void instantiateController_addInitCallback() {
+        verify(mShellInit, times(1)).addInitCallback(any(), any());
+    }
+
+    @Test
     public void instantiateController_registerConfigChangeListener() {
         verify(mShellController, times(1)).addConfigurationChangeListener(any());
     }
@@ -1025,7 +1041,7 @@
         assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2.getKey())).isNotNull();
 
         // Switch users
-        mBubbleController.onUserChanged(secondUserId);
+        switchUser(secondUserId);
         assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
 
         // Give this user some bubbles
@@ -1042,6 +1058,41 @@
         verify(mDataRepository, times(2)).loadBubbles(anyInt(), any());
     }
 
+    @Test
+    public void testOnUserChanged_bubblesRestored() {
+        int firstUserId = mBubbleEntry.getStatusBarNotification().getUser().getIdentifier();
+        int secondUserId = mBubbleEntryUser11.getStatusBarNotification().getUser().getIdentifier();
+        // Mock current profile
+        when(mLockscreenUserManager.isCurrentProfile(firstUserId)).thenReturn(true);
+        when(mLockscreenUserManager.isCurrentProfile(secondUserId)).thenReturn(false);
+
+        mBubbleController.updateBubble(mBubbleEntry);
+        assertThat(mBubbleController.hasBubbles()).isTrue();
+        // We start with 1 bubble
+        assertThat(mBubbleData.getBubbles()).hasSize(1);
+
+        // Switch to second user
+        switchUser(secondUserId);
+
+        // Second user has no bubbles
+        assertThat(mBubbleController.hasBubbles()).isFalse();
+
+        // Send bubble update for first user, ensure it does not show up
+        mBubbleController.updateBubble(mBubbleEntry2);
+        assertThat(mBubbleController.hasBubbles()).isFalse();
+
+        // Start returning notif for first user again
+        when(mCommonNotifCollection.getAllNotifs()).thenReturn(Arrays.asList(mRow, mRow2));
+
+        // Switch back to first user
+        switchUser(firstUserId);
+
+        // Check we now have two bubbles, one previous and one new that came in
+        assertThat(mBubbleController.hasBubbles()).isTrue();
+        // Now there are 2 bubbles
+        assertThat(mBubbleData.getBubbles()).hasSize(2);
+    }
+
     /**
      * Verifies we only load the overflow data once.
      */
@@ -1443,6 +1494,14 @@
                 .build();
     }
 
+    private void switchUser(int userId) {
+        when(mLockscreenUserManager.isCurrentProfile(anyInt())).thenAnswer(
+                (Answer<Boolean>) invocation -> invocation.<Integer>getArgument(0) == userId);
+        SparseArray<UserInfo> userInfos = new SparseArray<>(1);
+        userInfos.put(userId, mock(UserInfo.class));
+        mBubbleController.onCurrentProfilesChanged(userInfos);
+        mBubbleController.onUserChanged(userId);
+    }
 
     /**
      * Asserts that the bubble stack is expanded and also validates the cached state is updated.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
index f901c32..880ad187 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -39,6 +39,7 @@
 import com.android.wm.shell.draganddrop.DragAndDropController;
 import com.android.wm.shell.onehanded.OneHandedController;
 import com.android.wm.shell.sysui.ShellController;
+import com.android.wm.shell.sysui.ShellInit;
 
 import java.util.Optional;
 
@@ -49,6 +50,7 @@
 
     // Let's assume surfaces can be synchronized immediately.
     TestableBubbleController(Context context,
+            ShellInit shellInit,
             ShellController shellController,
             BubbleData data,
             FloatingContentCoordinator floatingContentCoordinator,
@@ -69,13 +71,13 @@
             Handler shellMainHandler,
             TaskViewTransitions taskViewTransitions,
             SyncTransactionQueue syncQueue) {
-        super(context, shellController, data, Runnable::run, floatingContentCoordinator,
+        super(context, shellInit, shellController, data, Runnable::run, floatingContentCoordinator,
                 dataRepository, statusBarService, windowManager, windowManagerShellWrapper,
                 userManager, launcherApps, bubbleLogger, taskStackListener, shellTaskOrganizer,
                 positioner, displayController, oneHandedOptional, dragAndDropController,
                 shellMainExecutor, shellMainHandler, new SyncExecutor(), taskViewTransitions,
                 syncQueue);
         setInflateSynchronously(true);
-        initialize();
+        onInit();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
index d80ea15..9635faf 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/TestableNotificationInterruptStateProviderImpl.java
@@ -24,7 +24,6 @@
 
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.notification.NotifPipelineFlags;
-import com.android.systemui.statusbar.notification.NotificationFilter;
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
 import com.android.systemui.statusbar.notification.interruption.NotificationInterruptStateProviderImpl;
@@ -40,7 +39,6 @@
             PowerManager powerManager,
             IDreamManager dreamManager,
             AmbientDisplayConfiguration ambientDisplayConfiguration,
-            NotificationFilter filter,
             StatusBarStateController statusBarStateController,
             KeyguardStateController keyguardStateController,
             BatteryController batteryController,
@@ -53,7 +51,6 @@
                 powerManager,
                 dreamManager,
                 ambientDisplayConfiguration,
-                filter,
                 batteryController,
                 statusBarStateController,
                 keyguardStateController,
diff --git a/packages/SystemUI/tests/utils/AndroidManifest.xml b/packages/SystemUI/tests/utils/AndroidManifest.xml
new file mode 100644
index 0000000..cbef5f6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+     Copyright (C) 2022 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.systemui.tests.utils">
+
+
+</manifest>
+
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiBaseFragmentTest.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/SysuiBaseFragmentTest.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/SysuiBaseFragmentTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestCase.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestableContext.java b/packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestableContext.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/SysuiTestableContext.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/SysuiTestableContext.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java b/packages/SystemUI/tests/utils/src/com/android/systemui/TestableDependency.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/TestableDependency.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/TestableDependency.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/broadcast/FakeBroadcastDispatcher.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingManagerFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/classifier/FalsingManagerFake.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/people/FakePeopleTileRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/people/FakePeopleTileRepository.kt
new file mode 100644
index 0000000..0bde5d2
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/people/FakePeopleTileRepository.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.people
+
+import com.android.systemui.people.data.model.PeopleTileModel
+import com.android.systemui.people.data.repository.PeopleTileRepository
+
+/** A fake [PeopleTileRepository] to be used in tests. */
+class FakePeopleTileRepository(
+    private val priorityTiles: List<PeopleTileModel>,
+    private val recentTiles: List<PeopleTileModel>,
+) : PeopleTileRepository {
+    override fun priorityTiles(): List<PeopleTileModel> = priorityTiles
+
+    override fun recentTiles(): List<PeopleTileModel> = recentTiles
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/people/FakePeopleWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/people/FakePeopleWidgetRepository.kt
new file mode 100644
index 0000000..2f81409
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/people/FakePeopleWidgetRepository.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.people
+
+import com.android.systemui.people.data.repository.PeopleWidgetRepository
+import com.android.systemui.people.widget.PeopleTileKey
+
+/** A fake [PeopleWidgetRepository] to be used in tests. */
+class FakePeopleWidgetRepository(
+    private val onSetWidgetTile: (widgetId: Int, tileKey: PeopleTileKey) -> Unit = { _, _ -> },
+) : PeopleWidgetRepository {
+    override fun setWidgetTile(widgetId: Int, tileKey: PeopleTileKey) {
+        onSetWidgetTile(widgetId, tileKey)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeScrollCaptureConnection.java b/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/FakeScrollCaptureConnection.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeScrollCaptureConnection.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/FakeScrollCaptureConnection.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeSession.java b/packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/FakeSession.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/screenshot/FakeSession.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/screenshot/FakeSession.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/RankingBuilder.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/RankingBuilder.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/SbnBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SbnBuilder.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/SbnBuilder.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/SbnBuilder.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotificationEntryBuilder.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/NotificationEntryBuilder.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotificationEntryBuilder.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/DeviceConfigProxyFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/DeviceConfigProxyFake.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/DeviceConfigProxyFake.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutor.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutor.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeExecutor.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeExecutor.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeRepeatableExecutor.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeRepeatableExecutor.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeRepeatableExecutor.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeRepeatableExecutor.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeThreadFactory.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeThreadFactory.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/concurrency/FakeThreadFactory.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/concurrency/FakeThreadFactory.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/FakeCondition.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/condition/FakeCondition.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/condition/FakeCondition.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/io/FakeBasicFileAttributes.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/io/FakeBasicFileAttributes.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/io/FakeBasicFileAttributes.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/io/FakeBasicFileAttributes.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeProximitySensor.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeProximitySensor.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeProximitySensor.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeSensorManager.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeSensorManager.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeSensorManager.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeSensorManager.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeThresholdSensor.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/sensors/FakeThresholdSensor.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/FakeThresholdSensor.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettings.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/settings/FakeSettings.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClock.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/time/FakeSystemClock.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClock.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/wakelock/WakeLockFake.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/util/wakelock/WakeLockFake.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/util/wakelock/WakeLockFake.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/BaseLeakChecker.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/BaseLeakChecker.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/BaseLeakChecker.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/BaseLeakChecker.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeBatteryController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeBatteryController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBatteryController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeBluetoothController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBluetoothController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeBluetoothController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeBluetoothController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeCastController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeCastController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeCastController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeCastController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeConfigurationController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeConfigurationController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeDataSaverController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeDataSaverController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeDataSaverController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeDataSaverController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeExtensionController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeExtensionController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeExtensionController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeExtensionController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeFlashlightController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeFlashlightController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeHotspotController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeHotspotController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeHotspotController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeHotspotController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeKeyguardStateController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeLocationController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeLocationController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeLocationController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeLocationController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeManagedProfileController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNetworkController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNetworkController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNextAlarmController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNextAlarmController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeNextAlarmController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeNextAlarmController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakePluginManager.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakePluginManager.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakePluginManager.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeRotationLockController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeRotationLockController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeSecurityController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeSecurityController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeSecurityController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeTunerService.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeTunerService.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeTunerService.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeTunerService.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeUserInfoController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeUserInfoController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeUserInfoController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeUserInfoController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeZenModeController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeZenModeController.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/FakeZenModeController.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeZenModeController.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckedTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/leaks/LeakCheckedTest.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckedTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/os/FakeHandler.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/os/FakeHandler.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/utils/os/FakeHandler.java
rename to packages/SystemUI/tests/utils/src/com/android/systemui/utils/os/FakeHandler.java
diff --git a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
index 803177b..3fa0ab6 100644
--- a/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
+++ b/services/accessibility/java/com/android/server/accessibility/SystemActionPerformer.java
@@ -16,8 +16,6 @@
 
 package com.android.server.accessibility;
 
-import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
-
 import android.accessibilityservice.AccessibilityService;
 import android.app.PendingIntent;
 import android.app.RemoteAction;
@@ -34,6 +32,7 @@
 import android.view.InputDevice;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
+import android.view.WindowManager;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 
 import com.android.internal.R;
@@ -392,8 +391,8 @@
     private boolean takeScreenshot() {
         ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null)
                 ? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext);
-        screenshotHelper.takeScreenshot(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
-                true, true, SCREENSHOT_ACCESSIBILITY_ACTIONS,
+        screenshotHelper.takeScreenshot(WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+                WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS,
                 new Handler(Looper.getMainLooper()), null);
         return true;
     }
diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
index 3ab873d..e07f412 100644
--- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
+++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java
@@ -439,6 +439,7 @@
                             log(MetricsEvent.TYPE_DISMISS);
                             hideFillDialogUiThread(callback);
                             callback.requestShowSoftInput(focusedId);
+                            callback.requestFallbackFromFillDialog();
                         }
 
                         @Override
diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java
index 7fe3c1f..606a09c 100644
--- a/services/core/java/com/android/server/am/BatteryStatsService.java
+++ b/services/core/java/com/android/server/am/BatteryStatsService.java
@@ -798,6 +798,7 @@
                 case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET:
                     final BatteryUsageStatsQuery querySinceReset =
                             new BatteryUsageStatsQuery.Builder()
+                                    .setMaxStatsAgeMs(0)
                                     .includeProcessStateData()
                                     .includeVirtualUids()
                                     .build();
@@ -806,6 +807,7 @@
                 case FrameworkStatsLog.BATTERY_USAGE_STATS_SINCE_RESET_USING_POWER_PROFILE_MODEL:
                     final BatteryUsageStatsQuery queryPowerProfile =
                             new BatteryUsageStatsQuery.Builder()
+                                    .setMaxStatsAgeMs(0)
                                     .includeProcessStateData()
                                     .includeVirtualUids()
                                     .powerProfileModeledOnly()
@@ -822,6 +824,7 @@
                     final long sessionEnd = mStats.getStartClockTime();
                     final BatteryUsageStatsQuery queryBeforeReset =
                             new BatteryUsageStatsQuery.Builder()
+                                    .setMaxStatsAgeMs(0)
                                     .includeProcessStateData()
                                     .includeVirtualUids()
                                     .aggregateSnapshots(sessionStart, sessionEnd)
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index a44a658..a6e73e4 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -3934,14 +3934,12 @@
             case AudioSystem.MODE_IN_COMMUNICATION:
             case AudioSystem.MODE_IN_CALL:
             case AudioSystem.MODE_NORMAL:
+            case AudioSystem.MODE_CALL_SCREENING:
+            case AudioSystem.MODE_CALL_REDIRECT:
+            case AudioSystem.MODE_COMMUNICATION_REDIRECT:
                 break;
-            case AudioSystem.MODE_RINGTONE:
-                // not changing anything for ringtone
-                return;
-            case AudioSystem.MODE_CURRENT:
-            case AudioSystem.MODE_INVALID:
             default:
-                // don't know what to do in this case, better bail
+                // no-op is enough for all other values
                 return;
         }
 
@@ -3964,6 +3962,34 @@
         }
     }
 
+    private void setLeAudioVolumeOnModeUpdate(int mode) {
+        switch (mode) {
+            case AudioSystem.MODE_IN_COMMUNICATION:
+            case AudioSystem.MODE_IN_CALL:
+            case AudioSystem.MODE_NORMAL:
+            case AudioSystem.MODE_CALL_SCREENING:
+            case AudioSystem.MODE_CALL_REDIRECT:
+            case AudioSystem.MODE_COMMUNICATION_REDIRECT:
+                break;
+            default:
+                // no-op is enough for all other values
+                return;
+        }
+
+        int streamType = getBluetoothContextualVolumeStream(mode);
+
+        // Currently, DEVICE_OUT_BLE_HEADSET is the only output type for LE_AUDIO profile.
+        // (See AudioDeviceBroker#createBtDeviceInfo())
+        int index = mStreamStates[streamType].getIndex(AudioSystem.DEVICE_OUT_BLE_HEADSET);
+        int maxIndex = mStreamStates[streamType].getMaxIndex();
+
+        if (DEBUG_VOL) {
+            Log.d(TAG, "setLeAudioVolumeOnModeUpdate postSetLeAudioVolumeIndex index="
+                    + index + " maxIndex=" + maxIndex + " streamType=" + streamType);
+        }
+        mDeviceBroker.postSetLeAudioVolumeIndex(index, maxIndex, streamType);
+    }
+
     private void setStreamVolume(int streamType, int index, int flags,
             @Nullable AudioDeviceAttributes ada,
             String callingPackage, String caller, String attributionTag, int uid,
@@ -5340,6 +5366,10 @@
                 // change of mode may require volume to be re-applied on some devices
                 updateAbsVolumeMultiModeDevices(previousMode, mode);
 
+                // Forcefully set LE audio volume as a workaround, since the value of 'device'
+                // is not DEVICE_OUT_BLE_* even when BLE is connected.
+                setLeAudioVolumeOnModeUpdate(mode);
+
                 // when entering RINGTONE, IN_CALL or IN_COMMUNICATION mode, clear all SCO
                 // connections not started by the application changing the mode when pid changes
                 mDeviceBroker.postSetModeOwnerPid(pid, mode);
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 95c8fef..62b1cfe 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1500,6 +1500,8 @@
         // Animate the screen brightness when the screen is on or dozing.
         // Skip the animation when the screen is off or suspended or transition to/from VR.
         boolean brightnessAdjusted = false;
+        final boolean brightnessIsTemporary =
+                mAppliedTemporaryBrightness || mAppliedTemporaryAutoBrightnessAdjustment;
         if (!mPendingScreenOff) {
             if (mSkipScreenOnBrightnessRamp) {
                 if (state == Display.STATE_ON) {
@@ -1532,8 +1534,6 @@
             // level without it being a noticeable jump since any actual content isn't yet visible.
             final boolean isDisplayContentVisible =
                     mColorFadeEnabled && mPowerState.getColorFadeLevel() == 1.0f;
-            final boolean brightnessIsTemporary =
-                    mAppliedTemporaryBrightness || mAppliedTemporaryAutoBrightnessAdjustment;
             // We only want to animate the brightness if it is between 0.0f and 1.0f.
             // brightnessState can contain the values -1.0f and NaN, which we do not want to
             // animate to. To avoid this, we check the value first.
@@ -1605,7 +1605,8 @@
             brightnessAdjusted = saveBrightnessInfo(getScreenBrightnessSetting());
         }
 
-        if (brightnessAdjusted) {
+        // Only notify if the brightness adjustment is not temporary (i.e. slider has been released)
+        if (brightnessAdjusted && !brightnessIsTemporary) {
             postBrightnessChangeRunnable();
         }
 
diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
index d34682d..1381614 100644
--- a/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
+++ b/services/core/java/com/android/server/pm/permission/PermissionManagerServiceImpl.java
@@ -1698,7 +1698,8 @@
                 mDefaultPermissionCallback.onInstallPermissionGranted();
             }
 
-            public void onPermissionRevoked(int uid, int userId, String reason) {
+            public void onPermissionRevoked(int uid, int userId, String reason,
+                    boolean overrideKill, @Nullable String permissionName) {
                 revokedPermissions.add(IntPair.of(uid, userId));
 
                 syncUpdatedUsers.add(userId);
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index ed32a7d..1f3f039 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2612,7 +2612,7 @@
         // either way, abort and reset the sequence.
         if (parcelable == null
                 || mTransferringSplashScreenState != TRANSFER_SPLASH_SCREEN_COPYING
-                || mStartingWindow == null
+                || mStartingWindow == null || mStartingWindow.mRemoved
                 || finishing) {
             if (parcelable != null) {
                 parcelable.clearIfNeeded();
@@ -4746,6 +4746,9 @@
             mPendingRemoteAnimation = options.getRemoteAnimationAdapter();
         }
         mPendingRemoteTransition = options.getRemoteTransition();
+        // Since options gets sent to client apps, remove transition information from it.
+        options.setRemoteTransition(null);
+        options.setRemoteAnimationAdapter(null);
     }
 
     void applyOptionsAnimation() {
@@ -9755,6 +9758,7 @@
                 record.mThumbnailAdapter != null ? record.mThumbnailAdapter.mCapturedLeash : null,
                 record.mStartBounds, task.getTaskInfo(), checkEnterPictureInPictureAppOpsState());
         target.setShowBackdrop(record.mShowBackdrop);
+        target.setWillShowImeOnTarget(mStartingData != null && mStartingData.hasImeSurface());
         target.hasAnimatingParent = record.hasAnimatingParent();
         return target;
     }
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index f967cf9..d9ab971 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -131,7 +131,6 @@
                         "Focused window found using getFocusedWindowToken");
             }
 
-            OnBackInvokedCallbackInfo overrideCallbackInfo = null;
             if (window != null) {
                 // This is needed to bridge the old and new back behavior with recents.  While in
                 // Overview with live tile enabled, the previous app is technically focused but we
@@ -140,15 +139,18 @@
                 // the right window to consume back while in overview, so we need to route it to
                 // launcher and use the legacy behavior of injecting KEYCODE_BACK since the existing
                 // compat callback in VRI only works when the window is focused.
+                // This symptom also happen while shell transition enabled, we can check that by
+                // isTransientLaunch to know whether the focus window is point to live tile.
                 final RecentsAnimationController recentsAnimationController =
                         wmService.getRecentsAnimationController();
-                if (recentsAnimationController != null
-                        && recentsAnimationController.shouldApplyInputConsumer(
-                        window.getActivityRecord())) {
-                    window = recentsAnimationController.getTargetAppMainWindow();
-                    overrideCallbackInfo = recentsAnimationController.getBackInvokedInfo();
+                final ActivityRecord ar = window.mActivityRecord;
+                if ((ar != null && ar.isActivityTypeHomeOrRecents()
+                        && ar.mTransitionController.isTransientLaunch(ar))
+                        || (recentsAnimationController != null
+                        && recentsAnimationController.shouldApplyInputConsumer(ar))) {
                     ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Current focused window being animated by "
                             + "recents. Overriding back callback to recents controller callback.");
+                    return null;
                 }
             }
 
@@ -166,9 +168,7 @@
             if (window != null) {
                 currentActivity = window.mActivityRecord;
                 currentTask = window.getTask();
-                callbackInfo = overrideCallbackInfo != null
-                        ? overrideCallbackInfo
-                        : window.getOnBackInvokedCallbackInfo();
+                callbackInfo = window.getOnBackInvokedCallbackInfo();
                 if (callbackInfo == null) {
                     Slog.e(TAG, "No callback registered, returning null.");
                     return null;
@@ -213,8 +213,10 @@
             Task finalTask = currentTask;
             prevActivity = currentTask.getActivity(
                     (r) -> !r.finishing && r.getTask() == finalTask && !r.isTopRunningActivity());
-            if (window.getParent().getChildCount() > 1 && window.getParent().getChildAt(0)
-                    != window) {
+            // TODO Dialog window does not need to attach on activity, check
+            // window.mAttrs.type != TYPE_BASE_APPLICATION
+            if ((window.getParent().getChildCount() > 1
+                    && window.getParent().getChildAt(0) != window)) {
                 // Are we the top window of our parent? If not, we are a window on top of the
                 // activity, we won't close the activity.
                 backType = BackNavigationInfo.TYPE_DIALOG_CLOSE;
@@ -379,7 +381,8 @@
 
     private void onBackNavigationDone(
             Bundle result, WindowState focusedWindow, WindowContainer<?> windowContainer,
-            int backType, Task task, ActivityRecord prevActivity, boolean prepareAnimation) {
+            int backType, @Nullable Task task, @Nullable ActivityRecord prevActivity,
+            boolean prepareAnimation) {
         SurfaceControl surfaceControl = windowContainer.getSurfaceControl();
         boolean triggerBack = result != null && result.getBoolean(
                 BackNavigationInfo.KEY_TRIGGER_BACK);
@@ -404,7 +407,7 @@
                         "Setting Activity.mLauncherTaskBehind to false. Activity=%s",
                         prevActivity);
             }
-        } else {
+        } else if (task != null) {
             task.mBackGestureStarted = false;
         }
         resetSurfaces(windowContainer);
diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java
index e6b5c20..7b4189b 100644
--- a/services/core/java/com/android/server/wm/DisplayPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayPolicy.java
@@ -318,12 +318,19 @@
      */
     private final ArrayList<WindowState> mStatusBarBackgroundWindows = new ArrayList<>();
 
+    /**
+     * A collection of {@link LetterboxDetails} of all visible activities to be sent to SysUI in
+     * order to determine status bar appearance
+     */
+    private final ArrayList<LetterboxDetails> mLetterboxDetails = new ArrayList<>();
+
     private String mFocusedApp;
     private int mLastDisableFlags;
     private int mLastAppearance;
     private int mLastBehavior;
     private InsetsVisibilities mRequestedVisibilities = new InsetsVisibilities();
     private AppearanceRegion[] mLastStatusBarAppearanceRegions;
+    private LetterboxDetails[] mLastLetterboxDetails;
 
     /** The union of checked bounds while building {@link #mStatusBarAppearanceRegionList}. */
     private final Rect mStatusBarColorCheckedBounds = new Rect();
@@ -1638,6 +1645,7 @@
         mNavBarColorWindowCandidate = null;
         mNavBarBackgroundWindow = null;
         mStatusBarAppearanceRegionList.clear();
+        mLetterboxDetails.clear();
         mStatusBarBackgroundWindows.clear();
         mStatusBarColorCheckedBounds.setEmpty();
         mStatusBarBackgroundCheckedBounds.setEmpty();
@@ -1717,6 +1725,16 @@
                             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);
+                        }
+                    }
                 }
             }
 
@@ -2404,12 +2422,15 @@
             callStatusBarSafely(statusBar -> statusBar.setDisableFlags(displayId, disableFlags,
                     cause));
         }
+        final LetterboxDetails[] letterboxDetails = new LetterboxDetails[mLetterboxDetails.size()];
+        mLetterboxDetails.toArray(letterboxDetails);
         if (mLastAppearance == appearance
                 && mLastBehavior == behavior
                 && mRequestedVisibilities.equals(win.getRequestedVisibilities())
                 && Objects.equals(mFocusedApp, focusedApp)
                 && mLastFocusIsFullscreen == isFullscreen
-                && Arrays.equals(mLastStatusBarAppearanceRegions, statusBarAppearanceRegions)) {
+                && Arrays.equals(mLastStatusBarAppearanceRegions, statusBarAppearanceRegions)
+                && Arrays.equals(mLastLetterboxDetails, letterboxDetails)) {
             return;
         }
         if (mDisplayContent.isDefaultDisplay && mLastFocusIsFullscreen != isFullscreen
@@ -2425,9 +2446,10 @@
         mFocusedApp = focusedApp;
         mLastFocusIsFullscreen = isFullscreen;
         mLastStatusBarAppearanceRegions = statusBarAppearanceRegions;
+        mLastLetterboxDetails = letterboxDetails;
         callStatusBarSafely(statusBar -> statusBar.onSystemBarAttributesChanged(displayId,
                 appearance, statusBarAppearanceRegions, isNavbarColorManagedByIme, behavior,
-                requestedVisibilities, focusedApp, new LetterboxDetails[]{}));
+                requestedVisibilities, focusedApp, letterboxDetails));
     }
 
     private void callStatusBarSafely(Consumer<StatusBarManagerInternal> consumer) {
@@ -2748,8 +2770,6 @@
     public void takeScreenshot(int screenshotType, int source) {
         if (mScreenshotHelper != null) {
             mScreenshotHelper.takeScreenshot(screenshotType,
-                    getStatusBar() != null && getStatusBar().isVisible(),
-                    getNavigationBar() != null && getNavigationBar().isVisible(),
                     source, mHandler, null /* completionConsumer */);
         }
     }
@@ -2843,6 +2863,12 @@
                 pw.print(prefixInner);  pw.println(mLastStatusBarAppearanceRegions[i]);
             }
         }
+        if (mLastLetterboxDetails != null) {
+            pw.print(prefix); pw.println("mLastLetterboxDetails=");
+            for (int i = mLastLetterboxDetails.length - 1; i >= 0; i--) {
+                pw.print(prefixInner);  pw.println(mLastLetterboxDetails[i]);
+            }
+        }
         if (!mStatusBarBackgroundWindows.isEmpty()) {
             pw.print(prefix); pw.println("mStatusBarBackgroundWindows=");
             for (int i = mStatusBarBackgroundWindows.size() - 1; i >= 0; i--) {
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index b5eff41..df3109a 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -138,6 +138,11 @@
         return mInner;
     }
 
+    /** @return The frame that contains the inner frame and the insets. */
+    Rect getOuterFrame() {
+        return mOuter;
+    }
+
     /**
      * Returns {@code true} if the letterbox does not overlap with the bar, or the letterbox can
      * fully cover the window frame.
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index d652767..ec9ee29 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -68,6 +68,7 @@
 
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.statusbar.LetterboxDetails;
 import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
 
 import java.io.PrintWriter;
@@ -141,6 +142,15 @@
         }
     }
 
+    /** Gets the outer bounds of letterbox. The bounds will be empty if there is no letterbox. */
+    private void getLetterboxOuterBounds(Rect outBounds) {
+        if (mLetterbox != null) {
+            outBounds.set(mLetterbox.getOuterFrame());
+        } else {
+            outBounds.setEmpty();
+        }
+    }
+
     /**
      * @return {@code true} if bar shown within a given rectangle is allowed to be fully transparent
      *     when the current activity is displayed.
@@ -683,4 +693,26 @@
         mActivityRecord.mTaskSupervisor.getActivityMetricsLogger()
                 .logLetterboxPositionChange(mActivityRecord, letterboxPositionChange);
     }
+
+    @Nullable
+    LetterboxDetails getLetterboxDetails() {
+        final WindowState w = mActivityRecord.findMainWindow();
+        if (mLetterbox == null || w == null || w.isLetterboxedForDisplayCutout()) {
+            return null;
+        }
+        Rect letterboxInnerBounds = new Rect();
+        Rect letterboxOuterBounds = new Rect();
+        getLetterboxInnerBounds(letterboxInnerBounds);
+        getLetterboxOuterBounds(letterboxOuterBounds);
+
+        if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) {
+            return null;
+        }
+
+        return new LetterboxDetails(
+                letterboxInnerBounds,
+                letterboxOuterBounds,
+                w.mAttrs.insetsFlags.appearance
+        );
+    }
 }
diff --git a/services/core/java/com/android/server/wm/RecentsAnimationController.java b/services/core/java/com/android/server/wm/RecentsAnimationController.java
index 53f1fe6..5b702ea 100644
--- a/services/core/java/com/android/server/wm/RecentsAnimationController.java
+++ b/services/core/java/com/android/server/wm/RecentsAnimationController.java
@@ -19,12 +19,10 @@
 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
-import static android.hardware.input.InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
 import static android.view.RemoteAnimationTarget.MODE_CLOSING;
 import static android.view.RemoteAnimationTarget.MODE_OPENING;
 import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-import static android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT;
 
 import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_RECENTS_ANIMATIONS;
 import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
@@ -41,7 +39,6 @@
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.hardware.HardwareBuffer;
-import android.hardware.input.InputManager;
 import android.os.Binder;
 import android.os.IBinder.DeathRecipient;
 import android.os.RemoteException;
@@ -54,18 +51,12 @@
 import android.util.proto.ProtoOutputStream;
 import android.view.IRecentsAnimationController;
 import android.view.IRecentsAnimationRunner;
-import android.view.InputDevice;
 import android.view.InputWindowHandle;
-import android.view.KeyCharacterMap;
-import android.view.KeyEvent;
 import android.view.RemoteAnimationTarget;
 import android.view.SurfaceControl;
 import android.view.SurfaceControl.Transaction;
 import android.view.SurfaceSession;
 import android.view.WindowInsets.Type;
-import android.window.BackEvent;
-import android.window.IOnBackInvokedCallback;
-import android.window.OnBackInvokedCallbackInfo;
 import android.window.PictureInPictureSurfaceTransaction;
 import android.window.TaskSnapshot;
 
@@ -195,46 +186,6 @@
         }
     };
 
-    /**
-     * Back invoked callback for legacy recents transition with the new back dispatch system.
-     */
-    final IOnBackInvokedCallback mBackCallback = new IOnBackInvokedCallback.Stub() {
-        @Override
-        public void onBackStarted() {
-            // Do nothing
-        }
-
-        @Override
-        public void onBackProgressed(BackEvent backEvent) {
-            // Do nothing
-        }
-
-        @Override
-        public void onBackCancelled() {
-            // Do nothing
-        }
-
-        @Override
-        public void onBackInvoked() {
-            sendBackEvent(KeyEvent.ACTION_DOWN);
-            sendBackEvent(KeyEvent.ACTION_UP);
-        }
-
-        private void sendBackEvent(int action) {
-            if (mTargetActivityRecord == null) {
-                return;
-            }
-            long when = SystemClock.uptimeMillis();
-            final KeyEvent ev = new KeyEvent(when, when, action,
-                    KeyEvent.KEYCODE_BACK, 0 /* repeat */, 0 /* metaState */,
-                    KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
-                    KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
-                    InputDevice.SOURCE_KEYBOARD);
-            ev.setDisplayId(mTargetActivityRecord.getDisplayId());
-            InputManager.getInstance().injectInputEvent(ev, INJECT_INPUT_EVENT_MODE_ASYNC);
-        }
-    };
-
     public interface RecentsAnimationCallbacks {
         /** Callback when recents animation is finished. */
         void onAnimationFinished(@ReorderMode int reorderMode, boolean sendUserLeaveHint);
@@ -1112,10 +1063,6 @@
         return mTargetActivityRecord.findMainWindow();
     }
 
-    OnBackInvokedCallbackInfo getBackInvokedInfo() {
-        return new OnBackInvokedCallbackInfo(mBackCallback, PRIORITY_DEFAULT);
-    }
-
     DisplayArea getTargetAppDisplayArea() {
         if (mTargetActivityRecord == null) {
             return null;
@@ -1236,6 +1183,12 @@
                     mLocalBounds, mBounds, mTask.getWindowConfiguration(),
                     mIsRecentTaskInvisible, null, null, mTask.getTaskInfo(),
                     topApp.checkEnterPictureInPictureAppOpsState());
+
+            final ActivityRecord topActivity = mTask.getTopNonFinishingActivity();
+            if (topActivity != null && topActivity.mStartingData != null
+                    && topActivity.mStartingData.hasImeSurface()) {
+                mTarget.setWillShowImeOnTarget(true);
+            }
             return mTarget;
         }
 
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index d91e2c8..3b25f28 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -401,6 +401,10 @@
         mTaskFragmentOrganizerProcessName = processName;
     }
 
+    void onTaskFragmentOrganizerRemoved() {
+        mTaskFragmentOrganizer = null;
+    }
+
     /** Whether this TaskFragment is organized by the given {@code organizer}. */
     boolean hasTaskFragmentOrganizer(ITaskFragmentOrganizer organizer) {
         return organizer != null && mTaskFragmentOrganizer != null
diff --git a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
index d4551be..602579f 100644
--- a/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskFragmentOrganizerController.java
@@ -136,6 +136,9 @@
         void dispose() {
             while (!mOrganizedTaskFragments.isEmpty()) {
                 final TaskFragment taskFragment = mOrganizedTaskFragments.get(0);
+                // Cleanup before remove to prevent it from sending any additional event, such as
+                // #onTaskFragmentVanished, to the removed organizer.
+                taskFragment.onTaskFragmentOrganizerRemoved();
                 taskFragment.removeImmediately();
                 mOrganizedTaskFragments.remove(taskFragment);
             }
@@ -512,10 +515,21 @@
         mPendingTaskFragmentEvents.add(pendingEvent);
     }
 
+    boolean isOrganizerRegistered(ITaskFragmentOrganizer organizer) {
+        return mTaskFragmentOrganizerState.containsKey(organizer.asBinder());
+    }
+
     private void removeOrganizer(ITaskFragmentOrganizer organizer) {
         final TaskFragmentOrganizerState state = validateAndGetState(organizer);
         // remove all of the children of the organized TaskFragment
         state.dispose();
+        // Remove any pending event of this organizer.
+        for (int i = mPendingTaskFragmentEvents.size() - 1; i >= 0; i--) {
+            final PendingTaskFragmentEvent event = mPendingTaskFragmentEvents.get(i);
+            if (event.mTaskFragmentOrg.asBinder().equals(organizer.asBinder())) {
+                mPendingTaskFragmentEvents.remove(i);
+            }
+        }
         mTaskFragmentOrganizerState.remove(organizer.asBinder());
     }
 
diff --git a/services/core/java/com/android/server/wm/TaskOrganizerController.java b/services/core/java/com/android/server/wm/TaskOrganizerController.java
index 8016658..c6989ef 100644
--- a/services/core/java/com/android/server/wm/TaskOrganizerController.java
+++ b/services/core/java/com/android/server/wm/TaskOrganizerController.java
@@ -632,6 +632,11 @@
         final Rect mainFrame = window.getRelativeFrame();
         final StartingWindowAnimationAdaptor adaptor = new StartingWindowAnimationAdaptor();
         window.startAnimation(t, adaptor, false, ANIMATION_TYPE_STARTING_REVEAL);
+        if (adaptor.mAnimationLeash == null) {
+            Slog.e(TAG, "Cannot start starting window animation, the window " + window
+                    + " was removed");
+            return null;
+        }
         t.setPosition(adaptor.mAnimationLeash, mainFrame.left, mainFrame.top);
         return adaptor.mAnimationLeash;
     }
@@ -679,13 +684,15 @@
         if (topActivity != null) {
             removalInfo.deferRemoveForIme = topActivity.mDisplayContent
                     .mayImeShowOnLaunchingActivity(topActivity);
-            if (removalInfo.playRevealAnimation && playShiftUpAnimation) {
-                final WindowState mainWindow =
-                        topActivity.findMainWindow(false/* includeStartingApp */);
-                if (mainWindow != null) {
-                    removalInfo.windowAnimationLeash = applyStartingWindowAnimation(mainWindow);
-                    removalInfo.mainFrame = mainWindow.getRelativeFrame();
-                }
+            final WindowState mainWindow =
+                    topActivity.findMainWindow(false/* includeStartingApp */);
+            // No app window for this activity, app might be crashed.
+            // Remove starting window immediately without playing reveal animation.
+            if (mainWindow == null || mainWindow.mRemoved) {
+                removalInfo.playRevealAnimation = false;
+            } else if (removalInfo.playRevealAnimation && playShiftUpAnimation) {
+                removalInfo.windowAnimationLeash = applyStartingWindowAnimation(mainWindow);
+                removalInfo.mainFrame = mainWindow.getRelativeFrame();
             }
         }
         try {
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 31d8eb8..bbc95a1 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -54,6 +54,7 @@
 import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER;
 import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT;
 import static android.window.TransitionInfo.FLAG_TRANSLUCENT;
+import static android.window.TransitionInfo.FLAG_WILL_IME_SHOWN;
 
 import static com.android.server.wm.ActivityRecord.State.RESUMED;
 import static com.android.server.wm.ActivityTaskManagerInternal.APP_TRANSITION_RECENTS_ANIM;
@@ -1728,6 +1729,13 @@
             if (task != null && task.voiceSession != null) {
                 flags |= FLAG_IS_VOICE_INTERACTION;
             }
+            if (task != null) {
+                final ActivityRecord topActivity = task.getTopNonFinishingActivity();
+                if (topActivity != null && topActivity.mStartingData != null
+                        && topActivity.mStartingData.hasImeSurface()) {
+                    flags |= FLAG_WILL_IME_SHOWN;
+                }
+            }
             final ActivityRecord record = wc.asActivityRecord();
             if (record != null) {
                 if (record.mUseTransferredAnimation) {
diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java
index 5c20258..4f324f2 100644
--- a/services/core/java/com/android/server/wm/TransitionController.java
+++ b/services/core/java/com/android/server/wm/TransitionController.java
@@ -479,9 +479,10 @@
         // Collect all visible non-app windows which need to be drawn before the animation starts.
         final DisplayContent dc = wc.asDisplayContent();
         if (dc != null) {
+            final boolean noAsyncRotation = dc.getAsyncRotationController() == null;
             wc.forAllWindows(w -> {
                 if (w.mActivityRecord == null && w.isVisible() && !isCollecting(w.mToken)
-                        && dc.shouldSyncRotationChange(w)) {
+                        && (noAsyncRotation || !AsyncRotationController.canBeAsync(w.mToken))) {
                     transition.collect(w.mToken);
                 }
             }, true /* traverseTopToBottom */);
diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java
index d9b25ad..2749b11 100644
--- a/services/core/java/com/android/server/wm/WindowContainer.java
+++ b/services/core/java/com/android/server/wm/WindowContainer.java
@@ -3773,9 +3773,9 @@
         // Check if this is changing displays. If so, mark the old display as "ready" for
         // transitions. This is to work around the problem where setting readiness against this
         // container will only set the new display as ready and leave the old display as unready.
-        if (mSyncState != SYNC_STATE_NONE && oldParent != null
-                && oldParent.getDisplayContent() != null && (newParent == null
-                        || oldParent.getDisplayContent() != newParent.getDisplayContent())) {
+        if (mSyncState != SYNC_STATE_NONE && oldParent != null && newParent != null
+                && oldParent.getDisplayContent() != null && newParent.getDisplayContent() != null
+                && oldParent.getDisplayContent() != newParent.getDisplayContent()) {
             mTransitionController.setReady(oldParent.getDisplayContent());
         }
 
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 48740a3..29e407f 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -385,6 +385,12 @@
     private void applyTransaction(@NonNull WindowContainerTransaction t, int syncId,
             @Nullable Transition transition, @NonNull CallerInfo caller,
             @Nullable Transition finishTransition) {
+        if (t.getTaskFragmentOrganizer() != null && !mTaskFragmentOrganizerController
+                .isOrganizerRegistered(t.getTaskFragmentOrganizer())) {
+            Slog.e(TAG, "Caller organizer=" + t.getTaskFragmentOrganizer()
+                    + " is no longer registered");
+            return;
+        }
         int effects = 0;
         ProtoLog.v(WM_DEBUG_WINDOW_ORGANIZER, "Apply window transaction, syncId=%d", syncId);
         mService.deferWindowLayout();
diff --git a/services/tests/servicestests/AndroidManifest.xml b/services/tests/servicestests/AndroidManifest.xml
index 0afb182..0483a60 100644
--- a/services/tests/servicestests/AndroidManifest.xml
+++ b/services/tests/servicestests/AndroidManifest.xml
@@ -103,6 +103,7 @@
     <uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
     <uses-permission android:name="android.permission.PACKAGE_VERIFICATION_AGENT" />
     <uses-permission android:name="android.permission.OBSERVE_ROLE_HOLDERS" />
+    <uses-permission android:name="android.permission.BATTERY_STATS" />
 
     <queries>
         <package android:name="com.android.servicestests.apps.suspendtestapp" />
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
index 1d6ed03..c15f6a9 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/SystemActionPerformerTest.java
@@ -17,6 +17,7 @@
 package com.android.server.accessibility;
 
 import static android.view.WindowManager.ScreenshotSource.SCREENSHOT_ACCESSIBILITY_ACTIONS;
+import static android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN;
 
 import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.is;
@@ -27,7 +28,6 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.never;
@@ -301,8 +301,9 @@
         mSystemActionPerformer.performSystemAction(
                 AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT);
         verify(mMockScreenshotHelper).takeScreenshot(
-                eq(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN), anyBoolean(),
-                anyBoolean(), eq(SCREENSHOT_ACCESSIBILITY_ACTIONS), any(Handler.class), any());
+                eq(TAKE_SCREENSHOT_FULLSCREEN),
+                eq(SCREENSHOT_ACCESSIBILITY_ACTIONS),
+                any(Handler.class), any());
     }
 
     // PendingIntent is a final class and cannot be mocked. So we are using this
diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsManagerTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsManagerTest.java
new file mode 100644
index 0000000..7ae1117
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsManagerTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.power.stats;
+
+import static androidx.test.InstrumentationRegistry.getContext;
+
+import static org.junit.Assert.fail;
+
+import android.os.BatteryConsumer;
+import android.os.BatteryStatsManager;
+import android.os.BatteryUsageStats;
+import android.os.BatteryUsageStatsQuery;
+import android.os.UidBatteryConsumer;
+
+import org.junit.Test;
+
+/**
+ * Test BatteryStatsManager and CellularBatteryStats to ensure that valid data is being reported
+ * and that invalid data is not reported.
+ */
+public class BatteryStatsManagerTest {
+
+    @Test
+    public void testBatteryUsageStatsDataConsistency() {
+        BatteryStatsManager bsm = getContext().getSystemService(BatteryStatsManager.class);
+        BatteryUsageStats stats = bsm.getBatteryUsageStats(
+                new BatteryUsageStatsQuery.Builder().setMaxStatsAgeMs(
+                        0).includeProcessStateData().build());
+        final int[] components =
+                {BatteryConsumer.POWER_COMPONENT_CPU,
+                        BatteryConsumer.POWER_COMPONENT_MOBILE_RADIO,
+                        BatteryConsumer.POWER_COMPONENT_WIFI,
+                        BatteryConsumer.POWER_COMPONENT_BLUETOOTH};
+        final int[] states =
+                {BatteryConsumer.PROCESS_STATE_FOREGROUND,
+                        BatteryConsumer.PROCESS_STATE_BACKGROUND,
+                        BatteryConsumer.PROCESS_STATE_FOREGROUND_SERVICE,
+                        BatteryConsumer.PROCESS_STATE_CACHED};
+        for (UidBatteryConsumer ubc : stats.getUidBatteryConsumers()) {
+            for (int component : components) {
+                double consumedPower = ubc.getConsumedPower(ubc.getKey(component));
+                double sumStates = 0;
+                for (int state : states) {
+                    sumStates += ubc.getConsumedPower(ubc.getKey(component, state));
+                }
+                if (sumStates > consumedPower + 0.1) {
+                    fail("Sum of states exceeds total. UID = " + ubc.getUid() + " "
+                            + BatteryConsumer.powerComponentIdToString(component)
+                            + " total = " + consumedPower + " states = " + sumStates);
+                }
+            }
+        }
+    }
+}
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 324e244..21839aa 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -61,8 +61,10 @@
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
@@ -82,11 +84,14 @@
 import android.platform.test.annotations.Presubmit;
 import android.provider.DeviceConfig;
 import android.provider.DeviceConfig.Properties;
+import android.view.InsetsVisibilities;
 import android.view.WindowManager;
 
 import androidx.test.filters.MediumTest;
 
 import com.android.internal.policy.SystemBarUtils;
+import com.android.internal.statusbar.LetterboxDetails;
+import com.android.server.statusbar.StatusBarManagerInternal;
 
 import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges;
 import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges;
@@ -97,6 +102,7 @@
 import org.junit.Test;
 import org.junit.rules.TestRule;
 import org.junit.runner.RunWith;
+import org.mockito.Mockito;
 
 /**
  * Tests for Size Compatibility mode.
@@ -2113,6 +2119,104 @@
         assertLetterboxSurfacesDrawnBetweenActivityAndParentBounds(organizer.mPrimary.getBounds());
     }
 
+    @Test
+    public void testLetterboxDetailsForStatusBar_noLetterbox() {
+        setUpDisplaySizeWithApp(2800, 1000);
+        addStatusBar(mActivity.mDisplayContent);
+        addWindowToActivity(mActivity); // Add a window to the activity so that we can get an
+        // appearance inside letterboxDetails
+
+        DisplayPolicy displayPolicy = mActivity.getDisplayContent().getDisplayPolicy();
+        StatusBarManagerInternal statusBar = displayPolicy.getStatusBarManagerInternal();
+        // We should get a null LetterboxDetails object as there is no letterboxed activity, so
+        // nothing will get passed to SysUI
+        verify(statusBar, never()).onSystemBarAttributesChanged(anyInt(), anyInt(),
+                any(), anyBoolean(), anyInt(),
+                any(InsetsVisibilities.class), isNull(), isNull());
+
+    }
+
+    @Test
+    public void testLetterboxDetailsForStatusBar_letterboxedForMaxAspectRatio() {
+        setUpDisplaySizeWithApp(2800, 1000);
+        addStatusBar(mActivity.mDisplayContent);
+        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(850, 0, 1950, 1000));
+
+        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 statusBar = displayPolicy.getStatusBarManagerInternal();
+        verify(statusBar).onSystemBarAttributesChanged(anyInt(), anyInt(),
+                any(), anyBoolean(), anyInt(),
+                any(InsetsVisibilities.class), isNull(), eq(expectedLetterboxDetails));
+    }
+
+    @Test
+    public void testSplitScreenLetterboxDetailsForStatusBar_twoLetterboxedApps() {
+        mAtm.mDevEnableNonResizableMultiWindow = true;
+        setUpDisplaySizeWithApp(2800, 1000);
+        addStatusBar(mActivity.mDisplayContent);
+        // Create another task for the second activity
+        final Task newTask = new TaskBuilder(mSupervisor).setDisplay(mActivity.getDisplayContent())
+                .setCreateActivity(true).build();
+        ActivityRecord newActivity = newTask.getTopNonFinishingActivity();
+
+        final TestSplitOrganizer organizer =
+                new TestSplitOrganizer(mAtm, mActivity.getDisplayContent());
+
+        // Move first activity to split screen which takes half of the screen.
+        organizer.mPrimary.setBounds(0, 0, 1400, 1000);
+        organizer.putTaskToPrimary(mTask, true);
+        // Move second activity to split screen which takes half of the screen.
+        organizer.mSecondary.setBounds(1400, 0, 2800, 1000);
+        organizer.putTaskToSecondary(newTask, true);
+
+        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);
+        addWindowToActivity(newActivity);
+        prepareUnresizable(newActivity, /* maxAspect */ 1.1f, SCREEN_ORIENTATION_UNSPECIFIED);
+
+        // Refresh the letterboxes
+        newActivity.mRootWindowContainer.performSurfacePlacement();
+
+        Rect mBounds = new Rect(mActivity.getWindowConfiguration().getBounds());
+        assertEquals(mBounds, new Rect(150, 0, 1250, 1000));
+        final Rect newBounds = new Rect(newActivity.getWindowConfiguration().getBounds());
+        assertEquals(newBounds, new Rect(1550, 0, 2650, 1000));
+
+        DisplayPolicy displayPolicy = mActivity.getDisplayContent().getDisplayPolicy();
+        LetterboxDetails[] expectedLetterboxDetails = { new LetterboxDetails(
+                mBounds,
+                organizer.mPrimary.getBounds(),
+                mActivity.findMainWindow().mAttrs.insetsFlags.appearance
+        ), new LetterboxDetails(
+                newBounds,
+                organizer.mSecondary.getBounds(),
+                newActivity.findMainWindow().mAttrs.insetsFlags.appearance
+        )};
+
+        // Check that letterboxDetails actually gets passed to SysUI
+        StatusBarManagerInternal statusBar = displayPolicy.getStatusBarManagerInternal();
+        verify(statusBar).onSystemBarAttributesChanged(anyInt(), anyInt(),
+                any(), anyBoolean(), anyInt(),
+                any(InsetsVisibilities.class), isNull(), eq(expectedLetterboxDetails));
+    }
+
     private void recomputeNaturalConfigurationOfUnresizableActivity() {
         // Recompute the natural configuration of the non-resizable activity and the split screen.
         mActivity.clearSizeCompatMode();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
index 97f0918..eba2755 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -420,6 +420,7 @@
     @Test
     public void testApplyTransaction_enforceHierarchyChange_setAdjacentRoots()
             throws RemoteException {
+        mAtm.mTaskFragmentOrganizerController.registerOrganizer(mIOrganizer);
         final TaskFragment taskFragment2 =
                 new TaskFragment(mAtm, new Binder(), true /* createdByOrganizer */);
         final WindowContainerToken token2 = taskFragment2.mRemoteToken.toWindowContainerToken();
@@ -595,6 +596,25 @@
     }
 
     @Test
+    public void testApplyTransaction_skipTransactionForUnregisterOrganizer() {
+        final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent);
+        final IBinder fragmentToken = new Binder();
+
+        // Allow organizer to create TaskFragment and start/reparent activity to TaskFragment.
+        createTaskFragmentFromOrganizer(mTransaction, ownerActivity, fragmentToken);
+        mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
+
+        // Nothing should happen as the organizer is not registered.
+        assertNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken));
+
+        mController.registerOrganizer(mIOrganizer);
+        mAtm.mWindowOrganizerController.applyTransaction(mTransaction);
+
+        // Successfully created when the organizer is registered.
+        assertNotNull(mAtm.mWindowOrganizerController.getTaskFragment(fragmentToken));
+    }
+
+    @Test
     public void testTaskFragmentInPip_startActivityInTaskFragment() {
         setupTaskFragmentInPip();
         final ActivityRecord activity = mTaskFragment.getTopMostActivity();