Merge "Add support to show apps on desktop in sysui proxy" into tm-qpr-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
index 5ef6855..30eacf3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/BatteryController.java
@@ -68,6 +68,11 @@
      */
     private final ArraySet<JobStatus> mChangedJobs = new ArraySet<>();
 
+    @GuardedBy("mLock")
+    private Boolean mLastReportedStatsdBatteryNotLow = null;
+    @GuardedBy("mLock")
+    private Boolean mLastReportedStatsdStablePower = null;
+
     public BatteryController(JobSchedulerService service) {
         super(service);
         mPowerTracker = new PowerTracker();
@@ -173,6 +178,19 @@
             Slog.d(TAG, "maybeReportNewChargingStateLocked: "
                     + powerConnected + "/" + stablePower + "/" + batteryNotLow);
         }
+
+        if (mLastReportedStatsdStablePower == null
+                || mLastReportedStatsdStablePower != stablePower) {
+            logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_CHARGING, stablePower);
+            mLastReportedStatsdStablePower = stablePower;
+        }
+        if (mLastReportedStatsdBatteryNotLow == null
+                || mLastReportedStatsdBatteryNotLow != stablePower) {
+            logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_BATTERY_NOT_LOW,
+                    batteryNotLow);
+            mLastReportedStatsdBatteryNotLow = batteryNotLow;
+        }
+
         final long nowElapsed = sElapsedRealtimeClock.millis();
         for (int i = mTrackedTasks.size() - 1; i >= 0; i--) {
             final JobStatus ts = mTrackedTasks.valueAt(i);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
index f6de109..abbe177 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -153,6 +153,8 @@
                 changed = true;
             }
             mDeviceIdleMode = enabled;
+            logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_DEVICE_NOT_DOZING,
+                    !mDeviceIdleMode);
             if (DEBUG) Slog.d(TAG, "mDeviceIdleMode=" + mDeviceIdleMode);
             mDeviceIdleUpdateFunctor.prepare();
             if (enabled) {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
index a6fae2c..8311dc3 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/IdleController.java
@@ -93,6 +93,8 @@
     @Override
     public void reportNewIdleState(boolean isIdle) {
         synchronized (mLock) {
+            logDeviceWideConstraintStateToStatsd(JobStatus.CONSTRAINT_IDLE, isIdle);
+
             final long nowElapsed = sElapsedRealtimeClock.millis();
             for (int i = mTrackedTasks.size()-1; i >= 0; i--) {
                 mTrackedTasks.valueAt(i).setIdleConstraintSatisfied(nowElapsed, isIdle);
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
index 866dc41..0d85dfd 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java
@@ -151,13 +151,12 @@
      */
     private static final int STATSD_CONSTRAINTS_TO_LOG = CONSTRAINT_CONTENT_TRIGGER
             | CONSTRAINT_DEADLINE
-            | CONSTRAINT_IDLE
             | CONSTRAINT_PREFETCH
             | CONSTRAINT_TARE_WEALTH
             | CONSTRAINT_TIMING_DELAY
             | CONSTRAINT_WITHIN_QUOTA;
 
-    // TODO(b/129954980)
+    // TODO(b/129954980): ensure this doesn't spam statsd, especially at boot
     private static final boolean STATS_LOG_ENABLED = false;
 
     // No override.
@@ -1864,7 +1863,7 @@
     }
 
     /** Returns a {@link JobServerProtoEnums.Constraint} enum value for the given constraint. */
-    private int getProtoConstraint(int constraint) {
+    static int getProtoConstraint(int constraint) {
         switch (constraint) {
             case CONSTRAINT_BACKGROUND_NOT_RESTRICTED:
                 return JobServerProtoEnums.CONSTRAINT_BACKGROUND_NOT_RESTRICTED;
@@ -1882,8 +1881,12 @@
                 return JobServerProtoEnums.CONSTRAINT_DEVICE_NOT_DOZING;
             case CONSTRAINT_IDLE:
                 return JobServerProtoEnums.CONSTRAINT_IDLE;
+            case CONSTRAINT_PREFETCH:
+                return JobServerProtoEnums.CONSTRAINT_PREFETCH;
             case CONSTRAINT_STORAGE_NOT_LOW:
                 return JobServerProtoEnums.CONSTRAINT_STORAGE_NOT_LOW;
+            case CONSTRAINT_TARE_WEALTH:
+                return JobServerProtoEnums.CONSTRAINT_TARE_WEALTH;
             case CONSTRAINT_TIMING_DELAY:
                 return JobServerProtoEnums.CONSTRAINT_TIMING_DELAY;
             case CONSTRAINT_WITHIN_QUOTA:
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
index 2a2d602..8453e53 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/StateController.java
@@ -26,6 +26,7 @@
 import android.util.proto.ProtoOutputStream;
 
 import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.job.JobSchedulerService;
 import com.android.server.job.JobSchedulerService.Constants;
 import com.android.server.job.StateChangedListener;
@@ -165,6 +166,15 @@
         return mService.areComponentsInPlaceLocked(jobStatus);
     }
 
+    protected void logDeviceWideConstraintStateToStatsd(int constraint, boolean satisfied) {
+        FrameworkStatsLog.write(
+                FrameworkStatsLog.DEVICE_WIDE_JOB_CONSTRAINT_CHANGED,
+                JobStatus.getProtoConstraint(constraint),
+                satisfied
+                        ? FrameworkStatsLog.DEVICE_WIDE_JOB_CONSTRAINT_CHANGED__STATE__SATISFIED
+                        : FrameworkStatsLog.DEVICE_WIDE_JOB_CONSTRAINT_CHANGED__STATE__UNSATISFIED);
+    }
+
     public abstract void dumpControllerStateLocked(IndentingPrintWriter pw,
             Predicate<JobStatus> predicate);
     public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
diff --git a/core/api/test-current.txt b/core/api/test-current.txt
index f5ee467..93c0c4d 100644
--- a/core/api/test-current.txt
+++ b/core/api/test-current.txt
@@ -3406,6 +3406,7 @@
     method @NonNull public android.window.WindowContainerTransaction createTaskFragment(@NonNull android.window.TaskFragmentCreationParams);
     method @NonNull public android.window.WindowContainerTransaction deleteTaskFragment(@NonNull android.window.WindowContainerToken);
     method public int describeContents();
+    method @NonNull public android.window.WindowContainerTransaction finishActivity(@NonNull android.os.IBinder);
     method @NonNull public android.window.WindowContainerTransaction removeTask(@NonNull android.window.WindowContainerToken);
     method @NonNull public android.window.WindowContainerTransaction reorder(@NonNull android.window.WindowContainerToken, boolean);
     method @NonNull public android.window.WindowContainerTransaction reparent(@NonNull android.window.WindowContainerToken, @Nullable android.window.WindowContainerToken, boolean);
diff --git a/core/java/Android.bp b/core/java/Android.bp
index a7d4342b..77589a2 100644
--- a/core/java/Android.bp
+++ b/core/java/Android.bp
@@ -15,6 +15,14 @@
         "**/*.java",
         "**/*.aidl",
     ],
+    exclude_srcs: [
+        // Remove election toolbar code from build time
+        "android/service/selectiontoolbar/*.aidl",
+        "android/service/selectiontoolbar/*.java",
+        "android/view/selectiontoolbar/*.aidl",
+        "android/view/selectiontoolbar/*.java",
+        "com/android/internal/widget/floatingtoolbar/RemoteFloatingToolbarPopup.java",
+    ],
     visibility: ["//frameworks/base"],
 }
 
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index ed06cae..b618969 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -228,8 +228,6 @@
 import android.view.contentcapture.IContentCaptureManager;
 import android.view.displayhash.DisplayHashManager;
 import android.view.inputmethod.InputMethodManager;
-import android.view.selectiontoolbar.ISelectionToolbarManager;
-import android.view.selectiontoolbar.SelectionToolbarManager;
 import android.view.textclassifier.TextClassificationManager;
 import android.view.textservice.TextServicesManager;
 import android.view.translation.ITranslationManager;
@@ -363,15 +361,6 @@
                 return new TextClassificationManager(ctx);
             }});
 
-        registerService(Context.SELECTION_TOOLBAR_SERVICE, SelectionToolbarManager.class,
-                new CachedServiceFetcher<SelectionToolbarManager>() {
-                    @Override
-                    public SelectionToolbarManager createService(ContextImpl ctx) {
-                        IBinder b = ServiceManager.getService(Context.SELECTION_TOOLBAR_SERVICE);
-                        return new SelectionToolbarManager(ctx.getOuterContext(),
-                                ISelectionToolbarManager.Stub.asInterface(b));
-                    }});
-
         registerService(Context.FONT_SERVICE, FontManager.class,
                 new CachedServiceFetcher<FontManager>() {
             @Override
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 163d6ed..4324442 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -20,6 +20,7 @@
 import android.annotation.Nullable;
 import android.annotation.TestApi;
 import android.app.Service;
+import android.content.ComponentName;
 import android.content.Intent;
 import android.os.IBinder;
 import android.os.RemoteException;
@@ -36,6 +37,7 @@
     private static final String TAG = "DreamOverlayService";
     private static final boolean DEBUG = false;
     private boolean mShowComplications;
+    private ComponentName mDreamComponent;
 
     private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
         @Override
@@ -56,6 +58,8 @@
     public final IBinder onBind(@NonNull Intent intent) {
         mShowComplications = intent.getBooleanExtra(DreamService.EXTRA_SHOW_COMPLICATIONS,
                 DreamService.DEFAULT_SHOW_COMPLICATIONS);
+        mDreamComponent = intent.getParcelableExtra(DreamService.EXTRA_DREAM_COMPONENT,
+                ComponentName.class);
         return mDreamOverlay.asBinder();
     }
 
@@ -84,4 +88,12 @@
     public final boolean shouldShowComplications() {
         return mShowComplications;
     }
+
+    /**
+     * Returns the active dream component.
+     * @hide
+     */
+    public final ComponentName getDreamComponent() {
+        return mDreamComponent;
+    }
 }
diff --git a/core/java/android/service/dreams/DreamService.java b/core/java/android/service/dreams/DreamService.java
index 7515538..d066ee7 100644
--- a/core/java/android/service/dreams/DreamService.java
+++ b/core/java/android/service/dreams/DreamService.java
@@ -218,6 +218,12 @@
             "android.service.dreams.SHOW_COMPLICATIONS";
 
     /**
+     * Extra containing the component name for the active dream.
+     * @hide
+     */
+    public static final String EXTRA_DREAM_COMPONENT = "android.service.dreams.DREAM_COMPONENT";
+
+    /**
      * The default value for whether to show complications on the overlay.
      * @hide
      */
@@ -271,6 +277,7 @@
             overlayIntent.setComponent(overlayService);
             overlayIntent.putExtra(EXTRA_SHOW_COMPLICATIONS,
                     fetchShouldShowComplications(context, serviceInfo));
+            overlayIntent.putExtra(EXTRA_DREAM_COMPONENT, dreamService);
 
             context.bindService(overlayIntent,
                     this, Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE);
diff --git a/core/java/android/service/voice/HotwordDetectionService.java b/core/java/android/service/voice/HotwordDetectionService.java
index d755d38..eac3bee 100644
--- a/core/java/android/service/voice/HotwordDetectionService.java
+++ b/core/java/android/service/voice/HotwordDetectionService.java
@@ -87,6 +87,14 @@
     public static final int MAXIMUM_NUMBER_OF_INITIALIZATION_STATUS_CUSTOM_ERROR = 2;
 
     /**
+     * Feature flag for Attention Service.
+     *
+     * TODO(b/247920386): Add TestApi annotation
+     * @hide
+     */
+    public static final boolean ENABLE_PROXIMITY_RESULT = false;
+
+    /**
      * Indicates that the updated status is successful.
      */
     public static final int INITIALIZATION_STATUS_SUCCESS = 0;
diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java
index 5832527..c8c941a 100644
--- a/core/java/android/view/InsetsSource.java
+++ b/core/java/android/view/InsetsSource.java
@@ -22,6 +22,7 @@
 import static android.view.InsetsSourceProto.VISIBLE_FRAME;
 import static android.view.InsetsState.ITYPE_CAPTION_BAR;
 import static android.view.InsetsState.ITYPE_IME;
+import static android.view.ViewRootImpl.CAPTION_ON_SHELL;
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -148,7 +149,7 @@
         // During drag-move and drag-resizing, the caption insets position may not get updated
         // before the app frame get updated. To layout the app content correctly during drag events,
         // we always return the insets with the corresponding height covering the top.
-        if (getType() == ITYPE_CAPTION_BAR) {
+        if (!CAPTION_ON_SHELL && getType() == ITYPE_CAPTION_BAR) {
             return Insets.of(0, frame.height(), 0, 0);
         }
         // Checks for whether there is shared edge with insets for 0-width/height window.
diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java
index ffbdf08..cfad1af 100644
--- a/core/java/android/window/WindowContainerTransaction.java
+++ b/core/java/android/window/WindowContainerTransaction.java
@@ -706,6 +706,23 @@
     }
 
     /**
+     * Finishes the Activity.
+     * Comparing to directly calling {@link android.app.Activity#finish()}, calling this can make
+     * sure the finishing happens in the same transaction with other operations.
+     * @param activityToken activity to be finished.
+     */
+    @NonNull
+    public WindowContainerTransaction finishActivity(@NonNull IBinder activityToken) {
+        final HierarchyOp hierarchyOp =
+                new HierarchyOp.Builder(
+                        HierarchyOp.HIERARCHY_OP_TYPE_FINISH_ACTIVITY)
+                        .setContainer(activityToken)
+                        .build();
+        mHierarchyOps.add(hierarchyOp);
+        return this;
+    }
+
+    /**
      * Sets/removes the always on top flag for this {@code windowContainer}. See
      * {@link com.android.server.wm.ConfigurationContainer#setAlwaysOnTop(boolean)}.
      * Please note that this method is only intended to be used for a
@@ -1163,6 +1180,7 @@
         public static final int HIERARCHY_OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT = 18;
         public static final int HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP = 19;
         public static final int HIERARCHY_OP_TYPE_REMOVE_TASK = 20;
+        public static final int HIERARCHY_OP_TYPE_FINISH_ACTIVITY = 21;
 
         // The following key(s) are for use with mLaunchOptions:
         // When launching a task (eg. from recents), this is the taskId to be launched.
@@ -1484,6 +1502,8 @@
                             + " alwaysOnTop=" + mAlwaysOnTop + "}";
                 case HIERARCHY_OP_TYPE_REMOVE_TASK:
                     return "{RemoveTask: task=" + mContainer + "}";
+                case HIERARCHY_OP_TYPE_FINISH_ACTIVITY:
+                    return "{finishActivity: activity=" + mContainer + "}";
                 default:
                     return "{mType=" + mType + " container=" + mContainer + " reparent=" + mReparent
                             + " mToTop=" + mToTop
diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java
index 8f0fc2d..620f177 100644
--- a/core/java/com/android/internal/app/ChooserActivity.java
+++ b/core/java/com/android/internal/app/ChooserActivity.java
@@ -1069,7 +1069,12 @@
         }
     }
 
-    private ViewGroup createContentPreviewView(ViewGroup parent) {
+    /**
+     * Create a view that will be shown in the content preview area
+     * @param parent reference to the parent container where the view should be attached to
+     * @return content preview view
+     */
+    protected ViewGroup createContentPreviewView(ViewGroup parent) {
         Intent targetIntent = getTargetIntent();
         int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
         return displayContentPreview(previewType, targetIntent, getLayoutInflater(), parent);
@@ -2640,7 +2645,7 @@
 
             boolean isExpandable = getResources().getConfiguration().orientation
                     == Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode();
-            if (directShareHeight != 0 && isSendAction(getTargetIntent())
+            if (directShareHeight != 0 && shouldShowContentPreview()
                     && isExpandable) {
                 // make sure to leave room for direct share 4->8 expansion
                 int requiredExpansionHeight =
@@ -2888,7 +2893,14 @@
         return shouldShowTabs()
                 && mMultiProfilePagerAdapter.getListAdapterForUserHandle(
                 UserHandle.of(UserHandle.myUserId())).getCount() > 0
-                && isSendAction(getTargetIntent());
+                && shouldShowContentPreview();
+    }
+
+    /**
+     * @return true if we want to show the content preview area
+     */
+    protected boolean shouldShowContentPreview() {
+        return isSendAction(getTargetIntent());
     }
 
     private void updateStickyContentPreview() {
@@ -3221,7 +3233,7 @@
                 return 0;
             }
 
-            if (!isSendAction(getTargetIntent())) {
+            if (!shouldShowContentPreview()) {
                 return 0;
             }
 
@@ -3252,7 +3264,7 @@
         // There can be at most one row in the listview, that is internally
         // a ViewGroup with 2 rows
         public int getServiceTargetRowCount() {
-            if (isSendAction(getTargetIntent())
+            if (shouldShowContentPreview()
                     && !ActivityManager.isLowRamDeviceStatic()) {
                 return 1;
             }
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt b/core/java/com/android/internal/app/ILogAccessDialogCallback.aidl
similarity index 60%
copy from packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
copy to core/java/com/android/internal/app/ILogAccessDialogCallback.aidl
index 6e17214..b2236c9 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
+++ b/core/java/com/android/internal/app/ILogAccessDialogCallback.aidl
@@ -14,15 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.systemui.compose.gallery
+package com.android.internal.app;
 
-import androidx.compose.foundation.layout.Column
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.android.systemui.ExampleFeature
-
-/** The screen that shows ExampleFeature. */
-@Composable
-fun ExampleFeatureScreen(modifier: Modifier = Modifier) {
-    Column(modifier) { ExampleFeature("This is an example feature!") }
-}
+/**
+ * IPC interface for an application to receive callbacks from the log access dialog callback.
+ */
+oneway interface ILogAccessDialogCallback {
+    void approveAccessForClient(int uid, String packageName);
+    void declineAccessForClient(int uid, String packageName);
+}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java b/core/java/com/android/internal/app/LogAccessDialogActivity.java
similarity index 71%
rename from services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
rename to core/java/com/android/internal/app/LogAccessDialogActivity.java
index 811e96c..4adb867 100644
--- a/services/core/java/com/android/server/logcat/LogAccessDialogActivity.java
+++ b/core/java/com/android/internal/app/LogAccessDialogActivity.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.server.logcat;
+package com.android.internal.app;
 
 import android.annotation.StyleRes;
 import android.app.Activity;
@@ -27,7 +27,14 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Handler;
+import android.os.RemoteException;
 import android.os.UserHandle;
+import android.text.Html;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.method.LinkMovementMethod;
+import android.text.style.TypefaceSpan;
+import android.text.style.URLSpan;
 import android.util.Slog;
 import android.view.ContextThemeWrapper;
 import android.view.InflateException;
@@ -37,7 +44,6 @@
 import android.widget.TextView;
 
 import com.android.internal.R;
-import com.android.server.LocalServices;
 
 /**
  * Dialog responsible for obtaining user consent per-use log access
@@ -45,17 +51,19 @@
 public class LogAccessDialogActivity extends Activity implements
         View.OnClickListener {
     private static final String TAG = LogAccessDialogActivity.class.getSimpleName();
+    public static final String EXTRA_CALLBACK = "EXTRA_CALLBACK";
+
 
     private static final int DIALOG_TIME_OUT = Build.IS_DEBUGGABLE ? 60000 : 300000;
     private static final int MSG_DISMISS_DIALOG = 0;
 
-    private final LogcatManagerService.LogcatManagerServiceInternal mLogcatManagerInternal =
-            LocalServices.getService(LogcatManagerService.LogcatManagerServiceInternal.class);
-
     private String mPackageName;
     private int mUid;
+    private ILogAccessDialogCallback mCallback;
 
     private String mAlertTitle;
+    private String mAlertBody;
+    private String mAlertLearnMore;
     private AlertDialog.Builder mAlertDialog;
     private AlertDialog mAlert;
     private View mAlertView;
@@ -81,6 +89,9 @@
             return;
         }
 
+        mAlertBody = getResources().getString(R.string.log_access_confirmation_body);
+        mAlertLearnMore = getResources().getString(R.string.log_access_confirmation_learn_more);
+
         // create View
         boolean isDarkTheme = (getResources().getConfiguration().uiMode
                 & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES;
@@ -118,6 +129,13 @@
             return false;
         }
 
+        mCallback = ILogAccessDialogCallback.Stub.asInterface(
+                intent.getExtras().getBinder(EXTRA_CALLBACK));
+        if (mCallback == null) {
+            Slog.e(TAG, "Missing callback");
+            return false;
+        }
+
         mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
         if (mPackageName == null || mPackageName.length() == 0) {
             Slog.e(TAG, "Missing package name extra");
@@ -165,13 +183,22 @@
         return titleString;
     }
 
+    private Spannable styleFont(String text) {
+        Spannable s = (Spannable) Html.fromHtml(text);
+        for (URLSpan span : s.getSpans(0, s.length(), URLSpan.class)) {
+            TypefaceSpan typefaceSpan = new TypefaceSpan("google-sans");
+            s.setSpan(typefaceSpan, s.getSpanStart(span), s.getSpanEnd(span), 0);
+        }
+        return s;
+    }
+
     /**
      * Returns the dialog view.
      * If we cannot retrieve the package name, it returns null and we decline the full device log
      * access
      */
     private View createView(@StyleRes int themeId) {
-        Context themedContext = new ContextThemeWrapper(getApplicationContext(), themeId);
+        Context themedContext = new ContextThemeWrapper(this, themeId);
         final View view = LayoutInflater.from(themedContext).inflate(
                 R.layout.log_access_user_consent_dialog_permission, null /*root*/);
 
@@ -182,6 +209,19 @@
         ((TextView) view.findViewById(R.id.log_access_dialog_title))
             .setText(mAlertTitle);
 
+        if (!TextUtils.isEmpty(mAlertLearnMore)) {
+            Spannable mSpannableLearnMore = styleFont(mAlertLearnMore);
+
+            ((TextView) view.findViewById(R.id.log_access_dialog_body))
+                    .setText(TextUtils.concat(mAlertBody, "\n\n", mSpannableLearnMore));
+
+            ((TextView) view.findViewById(R.id.log_access_dialog_body))
+                    .setMovementMethod(LinkMovementMethod.getInstance());
+        } else {
+            ((TextView) view.findViewById(R.id.log_access_dialog_body))
+                    .setText(mAlertBody);
+        }
+
         Button button_allow = (Button) view.findViewById(R.id.log_access_dialog_allow_button);
         button_allow.setOnClickListener(this);
 
@@ -194,19 +234,27 @@
 
     @Override
     public void onClick(View view) {
-        switch (view.getId()) {
-            case R.id.log_access_dialog_allow_button:
-                mLogcatManagerInternal.approveAccessForClient(mUid, mPackageName);
-                finish();
-                break;
-            case R.id.log_access_dialog_deny_button:
-                declineLogAccess();
-                finish();
-                break;
+        try {
+            switch (view.getId()) {
+                case R.id.log_access_dialog_allow_button:
+                    mCallback.approveAccessForClient(mUid, mPackageName);
+                    finish();
+                    break;
+                case R.id.log_access_dialog_deny_button:
+                    declineLogAccess();
+                    finish();
+                    break;
+            }
+        } catch (RemoteException e) {
+            finish();
         }
     }
 
     private void declineLogAccess() {
-        mLogcatManagerInternal.declineAccessForClient(mUid, mPackageName);
+        try {
+            mCallback.declineAccessForClient(mUid, mPackageName);
+        } catch (RemoteException e) {
+            finish();
+        }
     }
 }
diff --git a/core/java/com/android/internal/app/procstats/ProcessState.java b/core/java/com/android/internal/app/procstats/ProcessState.java
index 87e8ac1..72b9cd2 100644
--- a/core/java/com/android/internal/app/procstats/ProcessState.java
+++ b/core/java/com/android/internal/app/procstats/ProcessState.java
@@ -473,7 +473,10 @@
                 }
             }
             mCurCombinedState = state;
-            mStats.mUidStates.get(mUid).updateCombinedState(state, now);
+            final UidState uidState = mStats.mUidStates.get(mUid);
+            if (uidState != null) {
+                uidState.updateCombinedState(state, now);
+            }
         }
     }
 
diff --git a/core/java/com/android/internal/jank/InteractionJankMonitor.java b/core/java/com/android/internal/jank/InteractionJankMonitor.java
index 72de78c..708713b 100644
--- a/core/java/com/android/internal/jank/InteractionJankMonitor.java
+++ b/core/java/com/android/internal/jank/InteractionJankMonitor.java
@@ -29,7 +29,9 @@
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_APP_LAUNCH_FROM_WIDGET;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_OPEN_ALL_APPS;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_QUICK_SWITCH;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_LAUNCH_CAMERA;
+import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_PASSWORD_APPEAR;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_PASSWORD_DISAPPEAR;
 import static com.android.internal.util.FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_PATTERN_APPEAR;
@@ -219,6 +221,8 @@
     public static final int CUJ_TASKBAR_EXPAND = 60;
     public static final int CUJ_TASKBAR_COLLAPSE = 61;
     public static final int CUJ_SHADE_CLEAR_ALL = 62;
+    public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION = 63;
+    public static final int CUJ_LOCKSCREEN_OCCLUSION = 64;
 
     private static final int NO_STATSD_LOGGING = -1;
 
@@ -290,6 +294,8 @@
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_EXPAND,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__TASKBAR_COLLAPSE,
             UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__SHADE_CLEAR_ALL,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
+            UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LOCKSCREEN_OCCLUSION,
     };
 
     private static volatile InteractionJankMonitor sInstance;
@@ -372,7 +378,9 @@
             CUJ_USER_DIALOG_OPEN,
             CUJ_TASKBAR_EXPAND,
             CUJ_TASKBAR_COLLAPSE,
-            CUJ_SHADE_CLEAR_ALL
+            CUJ_SHADE_CLEAR_ALL,
+            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION,
+            CUJ_LOCKSCREEN_OCCLUSION
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
@@ -864,6 +872,10 @@
                 return "TASKBAR_COLLAPSE";
             case CUJ_SHADE_CLEAR_ALL:
                 return "SHADE_CLEAR_ALL";
+            case CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION:
+                return "LAUNCHER_UNLOCK_ENTRANCE_ANIMATION";
+            case CUJ_LOCKSCREEN_OCCLUSION:
+                return "LOCKSCREEN_OCCLUSION";
         }
         return "UNKNOWN";
     }
diff --git a/core/java/com/android/internal/util/LatencyTracker.java b/core/java/com/android/internal/util/LatencyTracker.java
index d3f9e0a..8fcb6d5 100644
--- a/core/java/com/android/internal/util/LatencyTracker.java
+++ b/core/java/com/android/internal/util/LatencyTracker.java
@@ -29,6 +29,7 @@
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_ROTATE_SCREEN_SENSOR;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_BACK_ARROW;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_SELECTION_TOOLBAR;
+import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_VOICE_INTERACTION;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_START_RECENTS_ANIMATION;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_SWITCH_DISPLAY_UNFOLD;
 import static com.android.internal.util.FrameworkStatsLog.UIACTION_LATENCY_REPORTED__ACTION__ACTION_TOGGLE_RECENTS;
@@ -174,6 +175,12 @@
      */
     public static final int ACTION_FOLD_TO_AOD = 18;
 
+    /**
+     * Time it takes to show the {@link android.service.voice.VoiceInteractionSession} system UI
+     * after a {@link android.hardware.soundtrigger3.ISoundTriggerHw} voice trigger.
+     */
+    public static final int ACTION_SHOW_VOICE_INTERACTION = 19;
+
     private static final int[] ACTIONS_ALL = {
         ACTION_EXPAND_PANEL,
         ACTION_TOGGLE_RECENTS,
@@ -194,6 +201,7 @@
         ACTION_LOAD_SHARE_SHEET,
         ACTION_SHOW_SELECTION_TOOLBAR,
         ACTION_FOLD_TO_AOD,
+        ACTION_SHOW_VOICE_INTERACTION,
     };
 
     /** @hide */
@@ -217,6 +225,7 @@
         ACTION_LOAD_SHARE_SHEET,
         ACTION_SHOW_SELECTION_TOOLBAR,
         ACTION_FOLD_TO_AOD,
+        ACTION_SHOW_VOICE_INTERACTION,
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface Action {
@@ -243,6 +252,7 @@
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_LOAD_SHARE_SHEET,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_SELECTION_TOOLBAR,
             UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD,
+            UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_VOICE_INTERACTION,
     };
 
     private static LatencyTracker sLatencyTracker;
@@ -340,6 +350,8 @@
                 return "ACTION_SHOW_SELECTION_TOOLBAR";
             case UIACTION_LATENCY_REPORTED__ACTION__ACTION_FOLD_TO_AOD:
                 return "ACTION_FOLD_TO_AOD";
+            case UIACTION_LATENCY_REPORTED__ACTION__ACTION_SHOW_VOICE_INTERACTION:
+                return "ACTION_SHOW_VOICE_INTERACTION";
             default:
                 throw new IllegalArgumentException("Invalid action");
         }
diff --git a/core/java/com/android/internal/widget/floatingtoolbar/FloatingToolbarPopup.java b/core/java/com/android/internal/widget/floatingtoolbar/FloatingToolbarPopup.java
index f7af67b..c484525 100644
--- a/core/java/com/android/internal/widget/floatingtoolbar/FloatingToolbarPopup.java
+++ b/core/java/com/android/internal/widget/floatingtoolbar/FloatingToolbarPopup.java
@@ -21,7 +21,6 @@
 import android.graphics.Rect;
 import android.view.MenuItem;
 import android.view.View;
-import android.view.selectiontoolbar.SelectionToolbarManager;
 import android.widget.PopupWindow;
 
 import java.util.List;
@@ -93,10 +92,7 @@
      * enabled, otherwise returns {@link LocalFloatingToolbarPopup} implementation.
      */
     static FloatingToolbarPopup createInstance(Context context, View parent) {
-        boolean enabled = SelectionToolbarManager.isRemoteSelectionToolbarEnabled(context);
-        return enabled
-                ? new RemoteFloatingToolbarPopup(context, parent)
-                : new LocalFloatingToolbarPopup(context, parent);
+        return new LocalFloatingToolbarPopup(context, parent);
     }
 
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index b91bd18..5ae133b 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -6783,8 +6783,9 @@
                   android:exported="false">
         </activity>
 
-        <activity android:name="com.android.server.logcat.LogAccessDialogActivity"
+        <activity android:name="com.android.internal.app.LogAccessDialogActivity"
                   android:theme="@style/Theme.Translucent.NoTitleBar"
+                  android:process=":ui"
                   android:excludeFromRecents="true"
                   android:exported="false">
         </activity>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6ec98e8..5763345 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -5755,10 +5755,21 @@
     <string name="log_access_confirmation_deny">Don\u2019t allow</string>
 
     <!-- Content for the log access confirmation dialog. [CHAR LIMIT=NONE]-->
-    <string name="log_access_confirmation_body">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
+    <string name="log_access_confirmation_body" product="default">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
         \n\nIf you don’t allow this app to access all device logs, it can still access its own logs. Your device manufacturer may still be able to access some logs or info on your device.
     </string>
 
+    <!-- Content for the log access confirmation dialog. [CHAR LIMIT=NONE]-->
+    <string name="log_access_confirmation_body" product="tv">Device logs record what happens on your device. Apps can use these logs to find and fix issues.\n\nSome logs may contain sensitive info, so only allow apps you trust to access all device logs.
+        \n\nIf you don’t allow this app to access all device logs, it can still access its own logs. Your device manufacturer may still be able to access some logs or info on your device.\n\nLearn more at g.co/android/devicelogs.
+    </string>
+
+    <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
+    <string name="log_access_confirmation_learn_more" product="default" translatable="false">&lt;a href="https://support.google.com/android?p=system_logs#topic=7313011"&gt;Learn more&lt;/a&gt;</string>
+
+    <!-- Learn more URL for the log access confirmation dialog. [DO NOT TRANSLATE]-->
+    <string name="log_access_confirmation_learn_more" product="tv" translatable="false"></string>
+
     <!-- Privacy notice do not show [CHAR LIMIT=20] -->
     <string name="log_access_do_not_show_again">Don\u2019t show again</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 546bb21..6e574bd 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3925,8 +3925,10 @@
   <java-symbol type="string" name="log_access_confirmation_deny" />
   <java-symbol type="string" name="log_access_confirmation_title" />
   <java-symbol type="string" name="log_access_confirmation_body" />
+  <java-symbol type="string" name="log_access_confirmation_learn_more" />
   <java-symbol type="layout" name="log_access_user_consent_dialog_permission" />
   <java-symbol type="id" name="log_access_dialog_title" />
+  <java-symbol type="id" name="log_access_dialog_body" />
   <java-symbol type="id" name="log_access_dialog_allow_button" />
   <java-symbol type="id" name="log_access_dialog_deny_button" />
 
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
index 626e0d9..18712ae 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskFragmentContainer.java
@@ -432,7 +432,7 @@
                     // In case we have requested to reparent the activity to another container (as
                     // pendingAppeared), we don't want to finish it with this container.
                     && mController.getContainerWithActivity(activity) == this) {
-                activity.finish();
+                wct.finishActivity(activity.getActivityToken());
             }
         }
 
@@ -457,7 +457,7 @@
                     || controller.shouldRetainAssociatedActivity(this, activity)) {
                 continue;
             }
-            activity.finish();
+            wct.finishActivity(activity.getActivityToken());
         }
         mActivitiesToFinishOnExit.clear();
     }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
index 179696a..25d0347 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java
@@ -207,7 +207,7 @@
 
         verify(mSplitPresenter, never()).deleteTaskFragment(any(), any());
         verify(mSplitController).removeContainer(tf);
-        verify(mActivity, never()).finish();
+        verify(mTransaction, never()).finishActivity(any());
     }
 
     @Test
@@ -1004,9 +1004,9 @@
         assertTrue(primaryContainer.isFinished());
         assertTrue(secondaryContainer0.isFinished());
         assertTrue(secondaryContainer1.isFinished());
-        verify(mActivity).finish();
-        verify(secondaryActivity0).finish();
-        verify(secondaryActivity1).finish();
+        verify(mTransaction).finishActivity(mActivity.getActivityToken());
+        verify(mTransaction).finishActivity(secondaryActivity0.getActivityToken());
+        verify(mTransaction).finishActivity(secondaryActivity1.getActivityToken());
         assertTrue(taskContainer.mContainers.isEmpty());
         assertTrue(taskContainer.mSplitContainers.isEmpty());
     }
diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
index 73428a2..35415d8 100644
--- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
+++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/TaskFragmentContainerTest.java
@@ -107,30 +107,29 @@
         final TaskFragmentContainer container = new TaskFragmentContainer(mActivity,
                 null /* pendingAppearedIntent */, taskContainer, mController);
         doReturn(container).when(mController).getContainerWithActivity(mActivity);
-        final WindowContainerTransaction wct = new WindowContainerTransaction();
 
         // Only remove the activity, but not clear the reference until appeared.
-        container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+        container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
 
-        verify(mActivity).finish();
+        verify(mTransaction).finishActivity(mActivity.getActivityToken());
         verify(mPresenter, never()).deleteTaskFragment(any(), any());
         verify(mController, never()).removeContainer(any());
 
         // Calling twice should not finish activity again.
-        clearInvocations(mActivity);
-        container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+        clearInvocations(mTransaction);
+        container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
 
-        verify(mActivity, never()).finish();
+        verify(mTransaction, never()).finishActivity(any());
         verify(mPresenter, never()).deleteTaskFragment(any(), any());
         verify(mController, never()).removeContainer(any());
 
         // Remove all references after the container has appeared in server.
         doReturn(new ArrayList<>()).when(mInfo).getActivities();
         container.setInfo(mTransaction, mInfo);
-        container.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
+        container.finish(true /* shouldFinishDependent */, mPresenter, mTransaction, mController);
 
-        verify(mActivity, never()).finish();
-        verify(mPresenter).deleteTaskFragment(wct, container.getTaskFragmentToken());
+        verify(mTransaction, never()).finishActivity(any());
+        verify(mPresenter).deleteTaskFragment(mTransaction, container.getTaskFragmentToken());
         verify(mController).removeContainer(container);
     }
 
@@ -150,7 +149,7 @@
         // The activity is requested to be reparented, so don't finish it.
         container0.finish(true /* shouldFinishDependent */, mPresenter, wct, mController);
 
-        verify(mActivity, never()).finish();
+        verify(mTransaction, never()).finishActivity(any());
         verify(mPresenter).deleteTaskFragment(wct, container0.getTaskFragmentToken());
         verify(mController).removeContainer(container0);
     }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index b5a5754..922472a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -59,6 +59,8 @@
 public class Bubble implements BubbleViewProvider {
     private static final String TAG = "Bubble";
 
+    public static final String KEY_APP_BUBBLE = "key_app_bubble";
+
     private final String mKey;
     @Nullable
     private final String mGroupKey;
@@ -164,6 +166,14 @@
     private PendingIntent mDeleteIntent;
 
     /**
+     * Used only for a special bubble in the stack that has the key {@link #KEY_APP_BUBBLE}.
+     * There can only be one of these bubbles in the stack and this intent will be populated for
+     * that bubble.
+     */
+    @Nullable
+    private Intent mAppIntent;
+
+    /**
      * Create a bubble with limited information based on given {@link ShortcutInfo}.
      * Note: Currently this is only being used when the bubble is persisted to disk.
      */
@@ -192,6 +202,22 @@
         mBubbleMetadataFlagListener = listener;
     }
 
+    public Bubble(Intent intent,
+            UserHandle user,
+            Executor mainExecutor) {
+        mKey = KEY_APP_BUBBLE;
+        mGroupKey = null;
+        mLocusId = null;
+        mFlags = 0;
+        mUser = user;
+        mShowBubbleUpdateDot = false;
+        mMainExecutor = mainExecutor;
+        mTaskId = INVALID_TASK_ID;
+        mAppIntent = intent;
+        mDesiredHeight = Integer.MAX_VALUE;
+        mPackageName = intent.getPackage();
+    }
+
     @VisibleForTesting(visibility = PRIVATE)
     public Bubble(@NonNull final BubbleEntry entry,
             final Bubbles.BubbleMetadataFlagListener listener,
@@ -417,6 +443,9 @@
 
         mShortcutInfo = info.shortcutInfo;
         mAppName = info.appName;
+        if (mTitle == null) {
+            mTitle = mAppName;
+        }
         mFlyoutMessage = info.flyoutMessage;
 
         mBadgeBitmap = info.badgeBitmap;
@@ -520,7 +549,7 @@
      * @return the last time this bubble was updated or accessed, whichever is most recent.
      */
     long getLastActivity() {
-        return Math.max(mLastUpdated, mLastAccessed);
+        return isAppBubble() ? Long.MAX_VALUE : Math.max(mLastUpdated, mLastAccessed);
     }
 
     /**
@@ -719,6 +748,15 @@
         return mDeleteIntent;
     }
 
+    @Nullable
+    Intent getAppBubbleIntent() {
+        return mAppIntent;
+    }
+
+    boolean isAppBubble() {
+        return KEY_APP_BUBBLE.equals(mKey);
+    }
+
     Intent getSettingsIntent(final Context context) {
         final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
         intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
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 0dfba34..93413db 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
@@ -1017,6 +1017,20 @@
     }
 
     /**
+     * Adds a bubble for a specific intent. These bubbles are <b>not</b> backed by a notification
+     * and remain until the user dismisses the bubble or bubble stack. Only one intent bubble
+     * is supported at a time.
+     *
+     * @param intent the intent to display in the bubble expanded view.
+     */
+    public void addAppBubble(Intent intent) {
+        if (intent == null || intent.getPackage() == null) return;
+        Bubble b = new Bubble(intent, UserHandle.of(mCurrentUserId), mMainExecutor);
+        b.setShouldAutoExpand(true);
+        inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+    }
+
+    /**
      * Fills the overflow bubbles by loading them from disk.
      */
     void loadOverflowBubblesFromDisk() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index 840b285..06f232c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -224,15 +224,23 @@
                 try {
                     options.setTaskAlwaysOnTop(true);
                     options.setLaunchedFromBubble(true);
-                    if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
+
+                    Intent fillInIntent = new Intent();
+                    // Apply flags to make behaviour match documentLaunchMode=always.
+                    fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
+                    fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+
+                    if (mBubble.isAppBubble()) {
+                        PendingIntent pi = PendingIntent.getActivity(mContext, 0,
+                                mBubble.getAppBubbleIntent(),
+                                PendingIntent.FLAG_MUTABLE,
+                                null);
+                        mTaskView.startActivity(pi, fillInIntent, options, launchBounds);
+                    } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
                         options.setApplyActivityFlagsForBubbles(true);
                         mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
                                 options, launchBounds);
                     } else {
-                        Intent fillInIntent = new Intent();
-                        // Apply flags to make behaviour match documentLaunchMode=always.
-                        fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
-                        fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
                         if (mBubble != null) {
                             mBubble.setIntentActive();
                         }
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 befac1e..f4dda4c 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
@@ -593,6 +593,7 @@
             ShellInit shellInit,
             ShellController shellController,
             ShellCommandHandler shellCommandHandler,
+            Optional<BubbleController> bubbleController,
             WindowManager windowManager,
             ShellTaskOrganizer organizer,
             TaskViewTransitions taskViewTransitions,
@@ -604,6 +605,7 @@
                     shellInit,
                     shellController,
                     shellCommandHandler,
+                    bubbleController,
                     windowManager,
                     organizer,
                     taskViewTransitions,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
index c79b9b8..6755299 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/floating/FloatingTasksController.java
@@ -39,6 +39,7 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.TaskViewTransitions;
+import com.android.wm.shell.bubbles.BubbleController;
 import com.android.wm.shell.common.RemoteCallable;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
@@ -54,6 +55,7 @@
 
 import java.io.PrintWriter;
 import java.util.Objects;
+import java.util.Optional;
 
 /**
  * Entry point for creating and managing floating tasks.
@@ -70,6 +72,8 @@
 
     public static final boolean FLOATING_TASKS_ENABLED =
             SystemProperties.getBoolean("persist.wm.debug.floating_tasks", false);
+    public static final boolean SHOW_FLOATING_TASKS_AS_BUBBLES =
+            SystemProperties.getBoolean("persist.wm.debug.floating_tasks_as_bubbles", false);
 
     @VisibleForTesting
     static final int SMALLEST_SCREEN_WIDTH_DP_TO_BE_TABLET = 600;
@@ -82,6 +86,7 @@
     private Context mContext;
     private ShellController mShellController;
     private ShellCommandHandler mShellCommandHandler;
+    private @Nullable BubbleController mBubbleController;
     private WindowManager mWindowManager;
     private ShellTaskOrganizer mTaskOrganizer;
     private TaskViewTransitions mTaskViewTransitions;
@@ -111,6 +116,7 @@
             ShellInit shellInit,
             ShellController shellController,
             ShellCommandHandler shellCommandHandler,
+            Optional<BubbleController> bubbleController,
             WindowManager windowManager,
             ShellTaskOrganizer organizer,
             TaskViewTransitions transitions,
@@ -120,6 +126,7 @@
         mContext = context;
         mShellController = shellController;
         mShellCommandHandler = shellCommandHandler;
+        mBubbleController = bubbleController.get();
         mWindowManager = windowManager;
         mTaskOrganizer = organizer;
         mTaskViewTransitions = transitions;
@@ -187,6 +194,22 @@
         return true;
     }
 
+    /** Returns true if the task was or should be shown as a bubble. */
+    private boolean maybeShowTaskAsBubble(Intent intent) {
+        if (SHOW_FLOATING_TASKS_AS_BUBBLES && mBubbleController != null) {
+            removeFloatingLayer();
+            if (intent.getPackage() != null) {
+                mBubbleController.addAppBubble(intent);
+                ProtoLog.d(WM_SHELL_FLOATING_APPS, "showing floating task as bubble: %s", intent);
+            } else {
+                ProtoLog.d(WM_SHELL_FLOATING_APPS,
+                        "failed to show floating task as bubble: %s; unknown package", intent);
+            }
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Shows, stashes, or un-stashes the floating task depending on state:
      * - If there is no floating task for this intent, it shows this the provided task.
@@ -195,6 +218,7 @@
      */
     public void showOrSetStashed(Intent intent) {
         if (!canShowTask(intent)) return;
+        if (maybeShowTaskAsBubble(intent)) return;
 
         addFloatingLayer();
 
@@ -215,6 +239,7 @@
      */
     public void showTask(Intent intent) {
         if (!canShowTask(intent)) return;
+        if (maybeShowTaskAsBubble(intent)) return;
 
         addFloatingLayer();
 
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 f47d2d3..6409e70 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
@@ -24,6 +24,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.TaskInfo;
+import android.content.ComponentName;
 import android.content.Context;
 import android.os.RemoteException;
 import android.util.Slog;
@@ -326,6 +327,28 @@
         return recentTasks;
     }
 
+    /**
+     * Find the background task that match the given component.
+     */
+    @Nullable
+    public ActivityManager.RecentTaskInfo findTaskInBackground(ComponentName componentName) {
+        if (componentName == null) {
+            return null;
+        }
+        List<ActivityManager.RecentTaskInfo> tasks = getRawRecentTasks(Integer.MAX_VALUE,
+                ActivityManager.RECENT_IGNORE_UNAVAILABLE, ActivityManager.getCurrentUser());
+        for (int i = 0; i < tasks.size(); i++) {
+            final ActivityManager.RecentTaskInfo task = tasks.get(i);
+            if (task.isVisible) {
+                continue;
+            }
+            if (componentName.equals(task.baseIntent.getComponent())) {
+                return task;
+            }
+        }
+        return null;
+    }
+
     public void dump(@NonNull PrintWriter pw, String prefix) {
         final String innerPrefix = prefix + "  ";
         pw.println(prefix + TAG);
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 991f136..07a6895 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
@@ -385,6 +385,9 @@
             }
             @Override
             public void onAnimationCancelled(boolean isKeyguardOccluded) {
+                final WindowContainerTransaction evictWct = new WindowContainerTransaction();
+                mStageCoordinator.prepareEvictInvisibleChildTasks(evictWct);
+                mSyncQueue.queue(evictWct);
             }
         };
         options = mStageCoordinator.resolveStartStage(STAGE_TYPE_UNDEFINED, position, options,
@@ -472,8 +475,16 @@
         fillInIntent.addFlags(FLAG_ACTIVITY_NO_USER_ACTION);
 
         // Flag with MULTIPLE_TASK if this is launching the same activity into both sides of the
-        // split.
+        // split and there is no reusable background task.
         if (shouldAddMultipleTaskFlag(intent.getIntent(), position)) {
+            final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional.isPresent()
+                    ? mRecentTasksOptional.get().findTaskInBackground(
+                            intent.getIntent().getComponent())
+                    : null;
+            if (taskInfo != null) {
+                startTask(taskInfo.taskId, position, options);
+                return;
+            }
             fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Adding MULTIPLE_TASK");
         }
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
index f1b4311..a88c837 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/floating/FloatingTasksControllerTest.java
@@ -62,6 +62,8 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.Optional;
+
 /**
  * Tests for the floating tasks controller.
  */
@@ -123,6 +125,7 @@
                 mShellInit,
                 mShellController,
                 mock(ShellCommandHandler.class),
+                Optional.empty(),
                 mWindowManager,
                 mTaskOrganizer,
                 mock(TaskViewTransitions.class),
diff --git a/media/TEST_MAPPING b/media/TEST_MAPPING
index 05fbc7a..23ee505 100644
--- a/media/TEST_MAPPING
+++ b/media/TEST_MAPPING
@@ -1,6 +1,9 @@
 {
   "presubmit": [
     {
+      "name": "mediaroutertest"
+    },
+    {
       "name": "CtsCameraTestCases",
       "options" : [
         {
diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java
index d8995b4..891ab45 100644
--- a/media/java/android/media/MediaRouter2.java
+++ b/media/java/android/media/MediaRouter2.java
@@ -48,6 +48,7 @@
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.stream.Collectors;
 
@@ -118,6 +119,7 @@
     private final Map<String, RoutingController> mNonSystemRoutingControllers = new ArrayMap<>();
 
     private final AtomicInteger mNextRequestId = new AtomicInteger(1);
+    private final AtomicBoolean mIsScanning = new AtomicBoolean(/* initialValue= */ false);
 
     final Handler mHandler;
 
@@ -234,7 +236,9 @@
     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
     public void startScan() {
         if (isSystemRouter()) {
-            sManager.startScan();
+            if (!mIsScanning.getAndSet(true)) {
+                sManager.registerScanRequest();
+            }
         }
     }
 
@@ -260,7 +264,9 @@
     @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL)
     public void stopScan() {
         if (isSystemRouter()) {
-            sManager.stopScan();
+            if (mIsScanning.getAndSet(false)) {
+                sManager.unregisterScanRequest();
+            }
         }
     }
 
diff --git a/media/java/android/media/MediaRouter2Manager.java b/media/java/android/media/MediaRouter2Manager.java
index 071667a..d79740c 100644
--- a/media/java/android/media/MediaRouter2Manager.java
+++ b/media/java/android/media/MediaRouter2Manager.java
@@ -79,9 +79,11 @@
     final String mPackageName;
 
     private final Context mContext;
-    @GuardedBy("sLock")
-    private Client mClient;
+
+    private final Client mClient;
+
     private final IMediaRouterService mMediaRouterService;
+    private final AtomicInteger mScanRequestCount = new AtomicInteger(/* initialValue= */ 0);
     final Handler mHandler;
     final CopyOnWriteArrayList<CallbackRecord> mCallbackRecords = new CopyOnWriteArrayList<>();
 
@@ -119,7 +121,12 @@
                 .getSystemService(Context.MEDIA_SESSION_SERVICE);
         mPackageName = mContext.getPackageName();
         mHandler = new Handler(context.getMainLooper());
-        mHandler.post(this::getOrCreateClient);
+        mClient = new Client();
+        try {
+            mMediaRouterService.registerManager(mClient, mPackageName);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -155,48 +162,47 @@
     }
 
     /**
-     * Starts scanning remote routes.
-     * <p>
-     * Route discovery can happen even when the {@link #startScan()} is not called.
-     * This is because the scanning could be started before by other apps.
-     * Therefore, calling this method after calling {@link #stopScan()} does not necessarily mean
-     * that the routes found before are removed and added again.
-     * <p>
-     * Use {@link Callback} to get the route related events.
-     * <p>
-     * @see #stopScan()
+     * Registers a request to scan for remote routes.
+     *
+     * <p>Increases the count of active scanning requests. When the count transitions from zero to
+     * one, sends a request to the system server to start scanning.
+     *
+     * <p>Clients must {@link #unregisterScanRequest() unregister their scan requests} when scanning
+     * is no longer needed, to avoid unnecessary resource usage.
      */
-    public void startScan() {
-        Client client = getOrCreateClient();
-        if (client != null) {
+    public void registerScanRequest() {
+        if (mScanRequestCount.getAndIncrement() == 0) {
             try {
-                mMediaRouterService.startScan(client);
+                mMediaRouterService.startScan(mClient);
             } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to get sessions. Service probably died.", ex);
+                throw ex.rethrowFromSystemServer();
             }
         }
     }
 
     /**
-     * Stops scanning remote routes to reduce resource consumption.
-     * <p>
-     * Route discovery can be continued even after this method is called.
-     * This is because the scanning is only turned off when all the apps stop scanning.
-     * Therefore, calling this method does not necessarily mean the routes are removed.
-     * Also, for the same reason it does not mean that {@link Callback#onRoutesAdded(List)}
-     * is not called afterwards.
-     * <p>
-     * Use {@link Callback} to get the route related events.
+     * Unregisters a scan request made by {@link #registerScanRequest()}.
      *
-     * @see #startScan()
+     * <p>Decreases the count of active scanning requests. When the count transitions from one to
+     * zero, sends a request to the system server to stop scanning.
+     *
+     * @throws IllegalStateException If called while there are no active scan requests.
      */
-    public void stopScan() {
-        Client client = getOrCreateClient();
-        if (client != null) {
+    public void unregisterScanRequest() {
+        if (mScanRequestCount.updateAndGet(
+                count -> {
+                    if (count == 0) {
+                        throw new IllegalStateException(
+                                "No active scan requests to unregister.");
+                    } else {
+                        return --count;
+                    }
+                })
+                == 0) {
             try {
-                mMediaRouterService.stopScan(client);
+                mMediaRouterService.stopScan(mClient);
             } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to get sessions. Service probably died.", ex);
+                throw ex.rethrowFromSystemServer();
             }
         }
     }
@@ -358,12 +364,10 @@
     @Nullable
     public RoutingSessionInfo getSystemRoutingSession(@Nullable String packageName) {
         try {
-            return mMediaRouterService.getSystemSessionInfoForPackage(
-                    getOrCreateClient(), packageName);
+            return mMediaRouterService.getSystemSessionInfoForPackage(mClient, packageName);
         } catch (RemoteException ex) {
-            Log.e(TAG, "Unable to get current system session info", ex);
+            throw ex.rethrowFromSystemServer();
         }
-        return null;
     }
 
     /**
@@ -424,15 +428,11 @@
      */
     @NonNull
     public List<RoutingSessionInfo> getRemoteSessions() {
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                return mMediaRouterService.getRemoteSessions(client);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to get sessions. Service probably died.", ex);
-            }
+        try {
+            return mMediaRouterService.getRemoteSessions(mClient);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
-        return Collections.emptyList();
     }
 
     /**
@@ -515,14 +515,11 @@
             return;
         }
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.setRouteVolumeWithManager(client, requestId, route, volume);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to set route volume.", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.setRouteVolumeWithManager(mClient, requestId, route, volume);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -544,15 +541,12 @@
             return;
         }
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.setSessionVolumeWithManager(
-                        client, requestId, sessionInfo.getId(), volume);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to set session volume.", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.setSessionVolumeWithManager(
+                    mClient, requestId, sessionInfo.getId(), volume);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -809,15 +803,12 @@
             return;
         }
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.selectRouteWithManager(
-                        client, requestId, sessionInfo.getId(), route);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "selectRoute: Failed to send a request.", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.selectRouteWithManager(
+                    mClient, requestId, sessionInfo.getId(), route);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -851,15 +842,12 @@
             return;
         }
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.deselectRouteWithManager(
-                        client, requestId, sessionInfo.getId(), route);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "deselectRoute: Failed to send a request.", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.deselectRouteWithManager(
+                    mClient, requestId, sessionInfo.getId(), route);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -876,15 +864,11 @@
     public void releaseSession(@NonNull RoutingSessionInfo sessionInfo) {
         Objects.requireNonNull(sessionInfo, "sessionInfo must not be null");
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                int requestId = mNextRequestId.getAndIncrement();
-                mMediaRouterService.releaseSessionWithManager(
-                        client, requestId, sessionInfo.getId());
-            } catch (RemoteException ex) {
-                Log.e(TAG, "releaseSession: Failed to send a request", ex);
-            }
+        try {
+            int requestId = mNextRequestId.getAndIncrement();
+            mMediaRouterService.releaseSessionWithManager(mClient, requestId, sessionInfo.getId());
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -897,14 +881,11 @@
             @NonNull MediaRoute2Info route) {
         int requestId = createTransferRequest(session, route);
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                mMediaRouterService.transferToRouteWithManager(
-                        client, requestId, session.getId(), route);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "transferToRoute: Failed to send a request.", ex);
-            }
+        try {
+            mMediaRouterService.transferToRouteWithManager(
+                    mClient, requestId, session.getId(), route);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -917,14 +898,11 @@
 
         int requestId = createTransferRequest(oldSession, route);
 
-        Client client = getOrCreateClient();
-        if (client != null) {
-            try {
-                mMediaRouterService.requestCreateSessionWithManager(
-                        client, requestId, oldSession, route);
-            } catch (RemoteException ex) {
-                Log.e(TAG, "requestCreateSession: Failed to send a request", ex);
-            }
+        try {
+            mMediaRouterService.requestCreateSessionWithManager(
+                    mClient, requestId, oldSession, route);
+        } catch (RemoteException ex) {
+            throw ex.rethrowFromSystemServer();
         }
     }
 
@@ -968,23 +946,6 @@
                 sessionInfo.getOwnerPackageName());
     }
 
-    private Client getOrCreateClient() {
-        synchronized (sLock) {
-            if (mClient != null) {
-                return mClient;
-            }
-            Client client = new Client();
-            try {
-                mMediaRouterService.registerManager(client, mPackageName);
-                mClient = client;
-                return client;
-            } catch (RemoteException ex) {
-                Log.e(TAG, "Unable to register media router manager.", ex);
-            }
-        }
-        return null;
-    }
-
     /**
      * Interface for receiving events about media routing changes.
      */
diff --git a/media/jni/android_media_MediaProfiles.cpp b/media/jni/android_media_MediaProfiles.cpp
index ecb1d51..9868a14 100644
--- a/media/jni/android_media_MediaProfiles.cpp
+++ b/media/jni/android_media_MediaProfiles.cpp
@@ -255,21 +255,21 @@
     jmethodID audioProfileConstructorMethodID =
         env->GetMethodID(audioProfileClazz, "<init>", "(IIIII)V");
 
-    jobjectArray videoCodecs = (jobjectArray)env->NewObjectArray(
-            cp->getVideoCodecs().size(), videoProfileClazz, nullptr);
+    jobjectArray videoCodecs = nullptr;
     {
-        int i = 0;
+        auto isAdvancedCodec = [](const MediaProfiles::VideoCodec *vc) -> bool {
+                                  return ((vc->getBitDepth() != 8
+                                        || vc->getChromaSubsampling() != CHROMA_SUBSAMPLING_YUV_420
+                                        || vc->getHdrFormat() != HDR_FORMAT_NONE));
+                              };
+        std::vector<jobject> codecVector;
         for (const MediaProfiles::VideoCodec *vc : cp->getVideoCodecs()) {
+            if (isAdvancedCodec(vc) && !static_cast<bool>(advanced)) {
+                continue;
+            }
             chroma_subsampling cs = vc->getChromaSubsampling();
             int bitDepth = vc->getBitDepth();
             hdr_format hdr = vc->getHdrFormat();
-
-            bool isAdvanced =
-                (bitDepth != 8 || cs != CHROMA_SUBSAMPLING_YUV_420 || hdr != HDR_FORMAT_NONE);
-            if (static_cast<bool>(advanced) && !isAdvanced) {
-                continue;
-            }
-
             jobject videoCodec = env->NewObject(videoProfileClazz,
                                                 videoProfileConstructorMethodID,
                                                 vc->getCodec(),
@@ -281,10 +281,17 @@
                                                 static_cast<int>(cs),
                                                 bitDepth,
                                                 static_cast<int>(hdr));
-            env->SetObjectArrayElement(videoCodecs, i++, videoCodec);
+
+            codecVector.push_back(videoCodec);
+        }
+        videoCodecs = (jobjectArray)env->NewObjectArray(codecVector.size(),
+                                                        videoProfileClazz, nullptr);
+
+        int i = 0;
+        for (jobject codecObj : codecVector) {
+             env->SetObjectArrayElement(videoCodecs, i++, codecObj);
         }
     }
-
     jobjectArray audioCodecs;
     if (quality >= CAMCORDER_QUALITY_TIME_LAPSE_LIST_START
             && quality <= CAMCORDER_QUALITY_TIME_LAPSE_LIST_END) {
diff --git a/media/tests/MediaRouter/Android.bp b/media/tests/MediaRouter/Android.bp
index 2da6c98..4f9c6f1 100644
--- a/media/tests/MediaRouter/Android.bp
+++ b/media/tests/MediaRouter/Android.bp
@@ -25,7 +25,7 @@
         "testng",
         "truth-prebuilt",
     ],
-
+    test_suites: ["general-tests"],
     platform_apis: true,
     certificate: "platform",
 }
diff --git a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
index b4aad9d..4086dec 100644
--- a/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
+++ b/media/tests/MediaRouter/src/com/android/mediaroutertest/MediaRouter2ManagerTest.java
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import android.Manifest;
@@ -121,7 +122,7 @@
         MediaRouter2ManagerTestActivity.startActivity(mContext);
 
         mManager = MediaRouter2Manager.getInstance(mContext);
-        mManager.startScan();
+        mManager.registerScanRequest();
         mRouter2 = MediaRouter2.getInstance(mContext);
 
         // If we need to support thread pool executors, change this to thread pool executor.
@@ -152,7 +153,7 @@
 
     @After
     public void tearDown() {
-        mManager.stopScan();
+        mManager.unregisterScanRequest();
 
         // order matters (callbacks should be cleared at the last)
         releaseAllSessions();
@@ -818,6 +819,13 @@
         assertFalse(failureLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
     }
 
+    @Test
+    public void unregisterScanRequest_enforcesANonNegativeCount() {
+        mManager.unregisterScanRequest(); // One request was made in the test setup.
+        assertThrows(IllegalStateException.class, () -> mManager.unregisterScanRequest());
+        mManager.registerScanRequest(); // So that the cleanup doesn't fail.
+    }
+
     /**
      * Tests if getSelectableRoutes and getDeselectableRoutes filter routes based on
      * selected routes
diff --git a/packages/CompanionDeviceManager/res/values-be/strings.xml b/packages/CompanionDeviceManager/res/values-be/strings.xml
index f448fbf..9cdd103 100644
--- a/packages/CompanionDeviceManager/res/values-be/strings.xml
+++ b/packages/CompanionDeviceManager/res/values-be/strings.xml
@@ -25,7 +25,7 @@
     <string name="permission_apps_summary" msgid="798718816711515431">"Трансліруйце змесціва праграм з вашага тэлефона"</string>
     <string name="title_app_streaming" msgid="2270331024626446950">"Дазвольце праграме &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; мець доступ да гэтай інфармацыі з вашага тэлефона"</string>
     <string name="helper_title_app_streaming" msgid="4151687003439969765">"Сэрвісы для некалькіх прылад"</string>
-    <string name="helper_summary_app_streaming" msgid="5977509499890099">"Праграма \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" запытвае дазвол ад імя вашай прылады \"<xliff:g id="DEVICE_TYPE">%2$s</xliff:g>\" на перадачу праграм плынню паміж вашымі прыладамі"</string>
+    <string name="helper_summary_app_streaming" msgid="5977509499890099">"Праграма \"<xliff:g id="APP_NAME">%1$s</xliff:g>\" запытвае дазвол ад імя вашай прылады \"<xliff:g id="DEVICE_TYPE">%2$s</xliff:g>\" на трансляцыю праграм паміж вашымі прыладамі"</string>
     <string name="title_automotive_projection" msgid="3296005598978412847"></string>
     <string name="summary_automotive_projection" msgid="8683801274662496164"></string>
     <string name="title_computer" msgid="4693714143506569253">"Дазвольце праграме &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; мець доступ да гэтай інфармацыі з вашага тэлефона"</string>
diff --git a/packages/CompanionDeviceManager/res/values-es/strings.xml b/packages/CompanionDeviceManager/res/values-es/strings.xml
index c781ea1..6d9ad9d 100644
--- a/packages/CompanionDeviceManager/res/values-es/strings.xml
+++ b/packages/CompanionDeviceManager/res/values-es/strings.xml
@@ -23,12 +23,12 @@
     <string name="summary_watch" msgid="3002344206574997652">"Se necesita esta aplicación para gestionar tu <xliff:g id="DEVICE_NAME">%1$s</xliff:g>. <xliff:g id="APP_NAME">%2$s</xliff:g> podrá interactuar con tus notificaciones y acceder a tus permisos de teléfono, SMS, contactos, calendario, registros de llamadas y dispositivos cercanos."</string>
     <string name="permission_apps" msgid="6142133265286656158">"Aplicaciones"</string>
     <string name="permission_apps_summary" msgid="798718816711515431">"Proyecta aplicaciones de tu teléfono"</string>
-    <string name="title_app_streaming" msgid="2270331024626446950">"Permitir que &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; acceda a esta información desde tu teléfono"</string>
+    <string name="title_app_streaming" msgid="2270331024626446950">"Permitir que &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; acceda a esta información de tu teléfono"</string>
     <string name="helper_title_app_streaming" msgid="4151687003439969765">"Servicios multidispositivo"</string>
     <string name="helper_summary_app_streaming" msgid="5977509499890099">"<xliff:g id="APP_NAME">%1$s</xliff:g> está pidiendo permiso en nombre de tu <xliff:g id="DEVICE_TYPE">%2$s</xliff:g> para emitir aplicaciones en otros dispositivos tuyos"</string>
     <string name="title_automotive_projection" msgid="3296005598978412847"></string>
     <string name="summary_automotive_projection" msgid="8683801274662496164"></string>
-    <string name="title_computer" msgid="4693714143506569253">"Permitir que &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; acceda a esta información desde tu teléfono"</string>
+    <string name="title_computer" msgid="4693714143506569253">"Permitir que &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; acceda a esta información de tu teléfono"</string>
     <string name="summary_computer" msgid="3798467601598297062"></string>
     <string name="permission_notification" msgid="693762568127741203">"Notificaciones"</string>
     <string name="permission_notification_summary" msgid="884075314530071011">"Puede leer todas las notificaciones, incluida información como contactos, mensajes y fotos"</string>
diff --git a/packages/CompanionDeviceManager/res/values-is/strings.xml b/packages/CompanionDeviceManager/res/values-is/strings.xml
index 3997deb..6f28275 100644
--- a/packages/CompanionDeviceManager/res/values-is/strings.xml
+++ b/packages/CompanionDeviceManager/res/values-is/strings.xml
@@ -25,13 +25,13 @@
     <string name="permission_apps_summary" msgid="798718816711515431">"Streymdu forritum símans"</string>
     <string name="title_app_streaming" msgid="2270331024626446950">"Veita &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; aðgang að þessum upplýsingum úr símanum þínum"</string>
     <string name="helper_title_app_streaming" msgid="4151687003439969765">"Þjónustur á milli tækja"</string>
-    <string name="helper_summary_app_streaming" msgid="5977509499890099">"<xliff:g id="APP_NAME">%1$s</xliff:g> sendir beiðni um heimild fyrir straumspilun forrita á milli tækjanna þinna fyrir hönd <xliff:g id="DEVICE_TYPE">%2$s</xliff:g>"</string>
+    <string name="helper_summary_app_streaming" msgid="5977509499890099">"<xliff:g id="APP_NAME">%1$s</xliff:g> sendir beiðni um heimild til straumspilunar forrita á milli tækjanna þinna fyrir hönd <xliff:g id="DEVICE_TYPE">%2$s</xliff:g>"</string>
     <string name="title_automotive_projection" msgid="3296005598978412847"></string>
     <string name="summary_automotive_projection" msgid="8683801274662496164"></string>
     <string name="title_computer" msgid="4693714143506569253">"Veita &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; aðgang að þessum upplýsingum úr símanum þínum"</string>
     <string name="summary_computer" msgid="3798467601598297062"></string>
     <string name="permission_notification" msgid="693762568127741203">"Tilkynningar"</string>
-    <string name="permission_notification_summary" msgid="884075314530071011">"Getur lesið allar tilkynningar, þar á meðal upplýsingar á borð við tengiliði skilaboð og myndir"</string>
+    <string name="permission_notification_summary" msgid="884075314530071011">"Getur lesið allar tilkynningar, þar á meðal upplýsingar á borð við tengiliði, skilaboð og myndir"</string>
     <string name="permission_storage" msgid="6831099350839392343">"Myndir og efni"</string>
     <string name="permission_storage_summary" msgid="3918240895519506417"></string>
     <string name="helper_title_computer" msgid="4671071173916176037">"Þjónusta Google Play"</string>
diff --git a/packages/CompanionDeviceManager/res/values-ky/strings.xml b/packages/CompanionDeviceManager/res/values-ky/strings.xml
index 24a3094..3e7b023 100644
--- a/packages/CompanionDeviceManager/res/values-ky/strings.xml
+++ b/packages/CompanionDeviceManager/res/values-ky/strings.xml
@@ -25,14 +25,14 @@
     <string name="permission_apps_summary" msgid="798718816711515431">"Телефондогу колдонмолорду алып ойнотуу"</string>
     <string name="title_app_streaming" msgid="2270331024626446950">"&lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; колдонмосуна телефонуңуздагы ушул маалыматты көрүүгө уруксат бериңиз"</string>
     <string name="helper_title_app_streaming" msgid="4151687003439969765">"Түзмөктөр аралык кызматтар"</string>
-    <string name="helper_summary_app_streaming" msgid="5977509499890099">"<xliff:g id="APP_NAME">%1$s</xliff:g> колдонмосу <xliff:g id="DEVICE_TYPE">%2$s</xliff:g> түзмөгүңүздүн атынан түзмөктөрүңүздүн ортосунда колдонмолорду тышкы экранга чыгарууга уруксат сурап жатат"</string>
+    <string name="helper_summary_app_streaming" msgid="5977509499890099">"<xliff:g id="APP_NAME">%1$s</xliff:g> <xliff:g id="DEVICE_TYPE">%2$s</xliff:g> түзмөгүңүздүн атынан түзмөктөрүңүздүн ортосунда колдонмолорду өткөрүүгө уруксат сурап жатат"</string>
     <string name="title_automotive_projection" msgid="3296005598978412847"></string>
     <string name="summary_automotive_projection" msgid="8683801274662496164"></string>
     <string name="title_computer" msgid="4693714143506569253">"&lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; колдонмосуна телефонуңуздагы ушул маалыматты көрүүгө уруксат бериңиз"</string>
     <string name="summary_computer" msgid="3798467601598297062"></string>
     <string name="permission_notification" msgid="693762568127741203">"Билдирмелер"</string>
     <string name="permission_notification_summary" msgid="884075314530071011">"Бардык билдирмелерди, анын ичинде байланыштар, билдирүүлөр жана сүрөттөр сыяктуу маалыматты окуй алат"</string>
-    <string name="permission_storage" msgid="6831099350839392343">"Сүрөттөр жана медиа"</string>
+    <string name="permission_storage" msgid="6831099350839392343">"Сүрөттөр жана медиафайлдар"</string>
     <string name="permission_storage_summary" msgid="3918240895519506417"></string>
     <string name="helper_title_computer" msgid="4671071173916176037">"Google Play кызматтары"</string>
     <string name="helper_summary_computer" msgid="9050724687678157852">"<xliff:g id="APP_NAME">%1$s</xliff:g> колдонмосу <xliff:g id="DEVICE_TYPE">%2$s</xliff:g> түзмөгүңүздүн атынан телефондогу сүрөттөрдү, медиа файлдарды жана билдирмелерди колдонууга уруксат сурап жатат"</string>
diff --git a/packages/CompanionDeviceManager/res/values-sk/strings.xml b/packages/CompanionDeviceManager/res/values-sk/strings.xml
index fa7155e..a7d8c5e 100644
--- a/packages/CompanionDeviceManager/res/values-sk/strings.xml
+++ b/packages/CompanionDeviceManager/res/values-sk/strings.xml
@@ -25,7 +25,7 @@
     <string name="permission_apps_summary" msgid="798718816711515431">"Streamovať aplikácie telefónu"</string>
     <string name="title_app_streaming" msgid="2270331024626446950">"Povoľte aplikácii &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; prístup k týmto informáciám z vášho telefónu"</string>
     <string name="helper_title_app_streaming" msgid="4151687003439969765">"Služby pre viacero zariadení"</string>
-    <string name="helper_summary_app_streaming" msgid="5977509499890099">"<xliff:g id="APP_NAME">%1$s</xliff:g> vyžaduje povolenie na streamovanie aplikácií medzi vašimi zariadeniami v mene tohto zariadenia (<xliff:g id="DEVICE_TYPE">%2$s</xliff:g>)"</string>
+    <string name="helper_summary_app_streaming" msgid="5977509499890099">"Aplikácia <xliff:g id="APP_NAME">%1$s</xliff:g> vyžaduje povolenie na streamovanie aplikácií medzi vašimi zariadeniami v mene tohto zariadenia (<xliff:g id="DEVICE_TYPE">%2$s</xliff:g>)"</string>
     <string name="title_automotive_projection" msgid="3296005598978412847"></string>
     <string name="summary_automotive_projection" msgid="8683801274662496164"></string>
     <string name="title_computer" msgid="4693714143506569253">"Povoľte aplikácii &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; prístup k týmto informáciám z vášho telefónu"</string>
diff --git a/packages/CompanionDeviceManager/res/values-sw/strings.xml b/packages/CompanionDeviceManager/res/values-sw/strings.xml
index 24465fc..966ff55 100644
--- a/packages/CompanionDeviceManager/res/values-sw/strings.xml
+++ b/packages/CompanionDeviceManager/res/values-sw/strings.xml
@@ -25,7 +25,7 @@
     <string name="permission_apps_summary" msgid="798718816711515431">"Tiririsha programu za simu yako"</string>
     <string name="title_app_streaming" msgid="2270331024626446950">"Ruhusu &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; ifikie maelezo haya kutoka kwenye simu yako"</string>
     <string name="helper_title_app_streaming" msgid="4151687003439969765">"Huduma za kifaa kilichounganishwa kwingine"</string>
-    <string name="helper_summary_app_streaming" msgid="5977509499890099">"<xliff:g id="APP_NAME">%1$s</xliff:g> inaomba ruhusa kwa niaba ya <xliff:g id="DEVICE_TYPE">%2$s</xliff:g> yako ili itiririshe programu kati ya vifaa vyako"</string>
+    <string name="helper_summary_app_streaming" msgid="5977509499890099">"Programu ya <xliff:g id="APP_NAME">%1$s</xliff:g> inaomba ruhusa kwa niaba ya <xliff:g id="DEVICE_TYPE">%2$s</xliff:g> yako ili itiririshe programu kati ya vifaa vyako"</string>
     <string name="title_automotive_projection" msgid="3296005598978412847"></string>
     <string name="summary_automotive_projection" msgid="8683801274662496164"></string>
     <string name="title_computer" msgid="4693714143506569253">"Ruhusu &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; ifikie maelezo haya kutoka kwenye simu yako"</string>
@@ -35,7 +35,7 @@
     <string name="permission_storage" msgid="6831099350839392343">"Picha na maudhui"</string>
     <string name="permission_storage_summary" msgid="3918240895519506417"></string>
     <string name="helper_title_computer" msgid="4671071173916176037">"Huduma za Google Play"</string>
-    <string name="helper_summary_computer" msgid="9050724687678157852">"<xliff:g id="APP_NAME">%1$s</xliff:g> inaomba ruhusa kwa niaba ya <xliff:g id="DEVICE_TYPE">%2$s</xliff:g> yako ili ifikie picha, maudhui na arifa za simu yako"</string>
+    <string name="helper_summary_computer" msgid="9050724687678157852">"Programu ya <xliff:g id="APP_NAME">%1$s</xliff:g> inaomba ruhusa kwa niaba ya <xliff:g id="DEVICE_TYPE">%2$s</xliff:g> yako ili ifikie picha, maudhui na arifa za simu yako"</string>
     <string name="profile_name_generic" msgid="6851028682723034988">"kifaa"</string>
     <string name="summary_generic" msgid="2346762210105903720"></string>
     <string name="consent_yes" msgid="8344487259618762872">"Ruhusu"</string>
diff --git a/packages/CompanionDeviceManager/res/values-te/strings.xml b/packages/CompanionDeviceManager/res/values-te/strings.xml
index 7440079..cd96095 100644
--- a/packages/CompanionDeviceManager/res/values-te/strings.xml
+++ b/packages/CompanionDeviceManager/res/values-te/strings.xml
@@ -31,7 +31,7 @@
     <string name="title_computer" msgid="4693714143506569253">"మీ ఫోన్ నుండి ఈ సమాచారాన్ని యాక్సెస్ చేయడానికి &lt;strong&gt;<xliff:g id="APP_NAME">%1$s</xliff:g>&lt;/strong&gt; యాప్‌ను అనుమతించండి"</string>
     <string name="summary_computer" msgid="3798467601598297062"></string>
     <string name="permission_notification" msgid="693762568127741203">"నోటిఫికేషన్‌లు"</string>
-    <string name="permission_notification_summary" msgid="884075314530071011">"కాంటాక్ట్‌లు, మెసేజ్‌లు, ఫోటోల వంటి సమాచారంతో సహా అన్ని నోటిఫికేషన్‌లను చదవగలరు"</string>
+    <string name="permission_notification_summary" msgid="884075314530071011">"కాంటాక్ట్‌లు, మెసేజ్‌లు, ఫోటోల వంటి సమాచారంతో సహా అన్ని నోటిఫికేషన్‌లను చదవగలదు"</string>
     <string name="permission_storage" msgid="6831099350839392343">"ఫోటోలు, మీడియా"</string>
     <string name="permission_storage_summary" msgid="3918240895519506417"></string>
     <string name="helper_title_computer" msgid="4671071173916176037">"Google Play సర్వీసులు"</string>
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
index fea7475..5c796af 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/BluetoothUtils.java
@@ -237,14 +237,10 @@
      * @return true if it supports advanced metadata, false otherwise.
      */
     public static boolean isAdvancedDetailsHeader(@NonNull BluetoothDevice bluetoothDevice) {
-        if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED,
-                true)) {
-            Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false");
+        if (!isAdvancedHeaderEnabled()) {
             return false;
         }
-        // The metadata is for Android R
-        if (getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
-            Log.d(TAG, "isAdvancedDetailsHeader: untetheredHeadset is true");
+        if (isUntetheredHeadset(bluetoothDevice)) {
             return true;
         }
         // The metadata is for Android S
@@ -260,6 +256,47 @@
     }
 
     /**
+     * Check if the Bluetooth device is supports advanced metadata and an untethered headset
+     *
+     * @param bluetoothDevice the BluetoothDevice to get metadata
+     * @return true if it supports advanced metadata and an untethered headset, false otherwise.
+     */
+    public static boolean isAdvancedUntetheredDevice(@NonNull BluetoothDevice bluetoothDevice) {
+        if (!isAdvancedHeaderEnabled()) {
+            return false;
+        }
+        if (isUntetheredHeadset(bluetoothDevice)) {
+            return true;
+        }
+        // The metadata is for Android S
+        String deviceType = getStringMetaData(bluetoothDevice,
+                BluetoothDevice.METADATA_DEVICE_TYPE);
+        if (TextUtils.equals(deviceType, BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET)) {
+            Log.d(TAG, "isAdvancedUntetheredDevice: is untethered device ");
+            return true;
+        }
+        return false;
+    }
+
+    private static boolean isAdvancedHeaderEnabled() {
+        if (!DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, BT_ADVANCED_HEADER_ENABLED,
+                true)) {
+            Log.d(TAG, "isAdvancedDetailsHeader: advancedEnabled is false");
+            return false;
+        }
+        return true;
+    }
+
+    private static boolean isUntetheredHeadset(@NonNull BluetoothDevice bluetoothDevice) {
+        // The metadata is for Android R
+        if (getBooleanMetaData(bluetoothDevice, BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)) {
+            Log.d(TAG, "isAdvancedDetailsHeader: untetheredHeadset is true");
+            return true;
+        }
+        return false;
+    }
+
+    /**
      * Create an Icon pointing to a drawable.
      */
     public static IconCompat createIconWithDrawable(Drawable drawable) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
index 1be9d76..3903404 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/BluetoothMediaDevice.java
@@ -59,14 +59,14 @@
 
     @Override
     public Drawable getIcon() {
-        return BluetoothUtils.isAdvancedDetailsHeader(mCachedDevice.getDevice())
+        return BluetoothUtils.isAdvancedUntetheredDevice(mCachedDevice.getDevice())
                 ? mContext.getDrawable(R.drawable.ic_earbuds_advanced)
                 : BluetoothUtils.getBtClassDrawableWithDescription(mContext, mCachedDevice).first;
     }
 
     @Override
     public Drawable getIconWithoutBackground() {
-        return BluetoothUtils.isAdvancedDetailsHeader(mCachedDevice.getDevice())
+        return BluetoothUtils.isAdvancedUntetheredDevice(mCachedDevice.getDevice())
                 ? mContext.getDrawable(R.drawable.ic_earbuds_advanced)
                 : BluetoothUtils.getBtClassDrawableWithDescription(mContext, mCachedDevice).first;
     }
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
index 58c15eb..7ec0fcd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InfoMediaManager.java
@@ -96,14 +96,14 @@
     public void startScan() {
         mMediaDevices.clear();
         mRouterManager.registerCallback(mExecutor, mMediaRouterCallback);
-        mRouterManager.startScan();
+        mRouterManager.registerScanRequest();
         refreshDevices();
     }
 
     @Override
     public void stopScan() {
         mRouterManager.unregisterCallback(mMediaRouterCallback);
-        mRouterManager.stopScan();
+        mRouterManager.unregisterScanRequest();
     }
 
     /**
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
index 1c0ea1a..ca14573 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/BluetoothUtilsTest.java
@@ -205,4 +205,45 @@
     public void isAdvancedDetailsHeader_noMetadata_returnFalse() {
         assertThat(BluetoothUtils.isAdvancedDetailsHeader(mBluetoothDevice)).isEqualTo(false);
     }
+
+    @Test
+    public void isAdvancedUntetheredDevice_untetheredHeadset_returnTrue() {
+        when(mBluetoothDevice.getMetadata(
+                BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET)).thenReturn(
+                BOOL_METADATA.getBytes());
+
+        assertThat(BluetoothUtils.isAdvancedUntetheredDevice(mBluetoothDevice)).isEqualTo(true);
+    }
+
+    @Test
+    public void isAdvancedUntetheredDevice_deviceTypeUntetheredHeadset_returnTrue() {
+        when(mBluetoothDevice.getMetadata(
+                BluetoothDevice.METADATA_DEVICE_TYPE)).thenReturn(
+                BluetoothDevice.DEVICE_TYPE_UNTETHERED_HEADSET.getBytes());
+
+        assertThat(BluetoothUtils.isAdvancedUntetheredDevice(mBluetoothDevice)).isEqualTo(true);
+    }
+
+    @Test
+    public void isAdvancedUntetheredDevice_deviceTypeWatch_returnFalse() {
+        when(mBluetoothDevice.getMetadata(
+                BluetoothDevice.METADATA_DEVICE_TYPE)).thenReturn(
+                BluetoothDevice.DEVICE_TYPE_WATCH.getBytes());
+
+        assertThat(BluetoothUtils.isAdvancedUntetheredDevice(mBluetoothDevice)).isEqualTo(false);
+    }
+
+    @Test
+    public void isAdvancedUntetheredDevice_deviceTypeDefault_returnFalse() {
+        when(mBluetoothDevice.getMetadata(
+                BluetoothDevice.METADATA_DEVICE_TYPE)).thenReturn(
+                BluetoothDevice.DEVICE_TYPE_DEFAULT.getBytes());
+
+        assertThat(BluetoothUtils.isAdvancedUntetheredDevice(mBluetoothDevice)).isEqualTo(false);
+    }
+
+    @Test
+    public void isAdvancedUntetheredDevice_noMetadata_returnFalse() {
+        assertThat(BluetoothUtils.isAdvancedUntetheredDevice(mBluetoothDevice)).isEqualTo(false);
+    }
 }
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index f50dc74..df6f08d 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -124,6 +124,7 @@
         "dagger2",
         "jsr330",
         "lottie",
+        "LowLightDreamLib",
     ],
     manifest: "AndroidManifest.xml",
 
@@ -227,6 +228,7 @@
         "dagger2",
         "jsr330",
         "WindowManager-Shell",
+        "LowLightDreamLib",
     ],
     libs: [
         "android.test.runner",
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/layout/pager/Pager.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/layout/pager/Pager.kt
new file mode 100644
index 0000000..19624e6
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/layout/pager/Pager.kt
@@ -0,0 +1,356 @@
+/*
+ * 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.compose.layout.pager
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.rememberSplineBasedDecay
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.Velocity
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filter
+
+/** Library-wide switch to turn on debug logging. */
+internal const val DebugLog = false
+
+@RequiresOptIn(message = "Accompanist Pager is experimental. The API may be changed in the future.")
+@Retention(AnnotationRetention.BINARY)
+annotation class ExperimentalPagerApi
+
+/** Contains the default values used by [HorizontalPager] and [VerticalPager]. */
+@ExperimentalPagerApi
+object PagerDefaults {
+    /**
+     * Remember the default [FlingBehavior] that represents the scroll curve.
+     *
+     * @param state The [PagerState] to update.
+     * @param decayAnimationSpec The decay animation spec to use for decayed flings.
+     * @param snapAnimationSpec The animation spec to use when snapping.
+     */
+    @Composable
+    fun flingBehavior(
+        state: PagerState,
+        decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
+        snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
+    ): FlingBehavior =
+        rememberSnappingFlingBehavior(
+            lazyListState = state.lazyListState,
+            decayAnimationSpec = decayAnimationSpec,
+            snapAnimationSpec = snapAnimationSpec,
+        )
+
+    @Deprecated(
+        "Replaced with PagerDefaults.flingBehavior()",
+        ReplaceWith("PagerDefaults.flingBehavior(state, decayAnimationSpec, snapAnimationSpec)")
+    )
+    @Composable
+    fun rememberPagerFlingConfig(
+        state: PagerState,
+        decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
+        snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
+    ): FlingBehavior = flingBehavior(state, decayAnimationSpec, snapAnimationSpec)
+}
+
+/**
+ * A horizontally scrolling layout that allows users to flip between items to the left and right.
+ *
+ * @sample com.google.accompanist.sample.pager.HorizontalPagerSample
+ *
+ * @param count the number of pages.
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the pager's state.
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the end to the start and [PagerState.currentPage] == 0 will mean the first item is
+ * located at the end.
+ * @param itemSpacing horizontal spacing to add between items.
+ * @param flingBehavior logic describing fling behavior.
+ * @param key the scroll position will be maintained based on the key, which means if you add/remove
+ * items before the current visible item the item with the given key will be kept as the first
+ * visible one.
+ * @param content a block which describes the content. Inside this block you can reference
+ * [PagerScope.currentPage] and other properties in [PagerScope].
+ */
+@ExperimentalPagerApi
+@Composable
+fun HorizontalPager(
+    count: Int,
+    modifier: Modifier = Modifier,
+    state: PagerState = rememberPagerState(),
+    reverseLayout: Boolean = false,
+    itemSpacing: Dp = 0.dp,
+    flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
+    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+    key: ((page: Int) -> Any)? = null,
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    content: @Composable PagerScope.(page: Int) -> Unit,
+) {
+    Pager(
+        count = count,
+        state = state,
+        modifier = modifier,
+        isVertical = false,
+        reverseLayout = reverseLayout,
+        itemSpacing = itemSpacing,
+        verticalAlignment = verticalAlignment,
+        flingBehavior = flingBehavior,
+        key = key,
+        contentPadding = contentPadding,
+        content = content
+    )
+}
+
+/**
+ * A vertically scrolling layout that allows users to flip between items to the top and bottom.
+ *
+ * @sample com.google.accompanist.sample.pager.VerticalPagerSample
+ *
+ * @param count the number of pages.
+ * @param modifier the modifier to apply to this layout.
+ * @param state the state object to be used to control or observe the pager's state.
+ * @param reverseLayout reverse the direction of scrolling and layout, when `true` items will be
+ * composed from the bottom to the top and [PagerState.currentPage] == 0 will mean the first item is
+ * located at the bottom.
+ * @param itemSpacing vertical spacing to add between items.
+ * @param flingBehavior logic describing fling behavior.
+ * @param key the scroll position will be maintained based on the key, which means if you add/remove
+ * items before the current visible item the item with the given key will be kept as the first
+ * visible one.
+ * @param content a block which describes the content. Inside this block you can reference
+ * [PagerScope.currentPage] and other properties in [PagerScope].
+ */
+@ExperimentalPagerApi
+@Composable
+fun VerticalPager(
+    count: Int,
+    modifier: Modifier = Modifier,
+    state: PagerState = rememberPagerState(),
+    reverseLayout: Boolean = false,
+    itemSpacing: Dp = 0.dp,
+    flingBehavior: FlingBehavior = PagerDefaults.flingBehavior(state),
+    horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+    key: ((page: Int) -> Any)? = null,
+    contentPadding: PaddingValues = PaddingValues(0.dp),
+    content: @Composable PagerScope.(page: Int) -> Unit,
+) {
+    Pager(
+        count = count,
+        state = state,
+        modifier = modifier,
+        isVertical = true,
+        reverseLayout = reverseLayout,
+        itemSpacing = itemSpacing,
+        horizontalAlignment = horizontalAlignment,
+        flingBehavior = flingBehavior,
+        key = key,
+        contentPadding = contentPadding,
+        content = content
+    )
+}
+
+@ExperimentalPagerApi
+@Composable
+internal fun Pager(
+    count: Int,
+    modifier: Modifier,
+    state: PagerState,
+    reverseLayout: Boolean,
+    itemSpacing: Dp,
+    isVertical: Boolean,
+    flingBehavior: FlingBehavior,
+    key: ((page: Int) -> Any)?,
+    contentPadding: PaddingValues,
+    verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
+    horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally,
+    content: @Composable PagerScope.(page: Int) -> Unit,
+) {
+    require(count >= 0) { "pageCount must be >= 0" }
+
+    // Provide our PagerState with access to the SnappingFlingBehavior animation target
+    // TODO: can this be done in a better way?
+    state.flingAnimationTarget = { (flingBehavior as? SnappingFlingBehavior)?.animationTarget }
+
+    LaunchedEffect(count) {
+        state.currentPage = minOf(count - 1, state.currentPage).coerceAtLeast(0)
+    }
+
+    // Once a fling (scroll) has finished, notify the state
+    LaunchedEffect(state) {
+        // When a 'scroll' has finished, notify the state
+        snapshotFlow { state.isScrollInProgress }
+            .filter { !it }
+            .collect { state.onScrollFinished() }
+    }
+
+    val pagerScope = remember(state) { PagerScopeImpl(state) }
+
+    // We only consume nested flings in the main-axis, allowing cross-axis flings to propagate
+    // as normal
+    val consumeFlingNestedScrollConnection =
+        ConsumeFlingNestedScrollConnection(
+            consumeHorizontal = !isVertical,
+            consumeVertical = isVertical,
+        )
+
+    if (isVertical) {
+        LazyColumn(
+            state = state.lazyListState,
+            verticalArrangement = Arrangement.spacedBy(itemSpacing, verticalAlignment),
+            horizontalAlignment = horizontalAlignment,
+            flingBehavior = flingBehavior,
+            reverseLayout = reverseLayout,
+            contentPadding = contentPadding,
+            modifier = modifier,
+        ) {
+            items(
+                count = count,
+                key = key,
+            ) { page ->
+                Box(
+                    Modifier
+                        // We don't any nested flings to continue in the pager, so we add a
+                        // connection which consumes them.
+                        // See: https://github.com/google/accompanist/issues/347
+                        .nestedScroll(connection = consumeFlingNestedScrollConnection)
+                        // Constraint the content to be <= than the size of the pager.
+                        .fillParentMaxHeight()
+                        .wrapContentSize()
+                ) { pagerScope.content(page) }
+            }
+        }
+    } else {
+        LazyRow(
+            state = state.lazyListState,
+            verticalAlignment = verticalAlignment,
+            horizontalArrangement = Arrangement.spacedBy(itemSpacing, horizontalAlignment),
+            flingBehavior = flingBehavior,
+            reverseLayout = reverseLayout,
+            contentPadding = contentPadding,
+            modifier = modifier,
+        ) {
+            items(
+                count = count,
+                key = key,
+            ) { page ->
+                Box(
+                    Modifier
+                        // We don't any nested flings to continue in the pager, so we add a
+                        // connection which consumes them.
+                        // See: https://github.com/google/accompanist/issues/347
+                        .nestedScroll(connection = consumeFlingNestedScrollConnection)
+                        // Constraint the content to be <= than the size of the pager.
+                        .fillParentMaxWidth()
+                        .wrapContentSize()
+                ) { pagerScope.content(page) }
+            }
+        }
+    }
+}
+
+private class ConsumeFlingNestedScrollConnection(
+    private val consumeHorizontal: Boolean,
+    private val consumeVertical: Boolean,
+) : NestedScrollConnection {
+    override fun onPostScroll(
+        consumed: Offset,
+        available: Offset,
+        source: NestedScrollSource
+    ): Offset =
+        when (source) {
+            // We can consume all resting fling scrolls so that they don't propagate up to the
+            // Pager
+            NestedScrollSource.Fling -> available.consume(consumeHorizontal, consumeVertical)
+            else -> Offset.Zero
+        }
+
+    override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+        // We can consume all post fling velocity on the main-axis
+        // so that it doesn't propagate up to the Pager
+        return available.consume(consumeHorizontal, consumeVertical)
+    }
+}
+
+private fun Offset.consume(
+    consumeHorizontal: Boolean,
+    consumeVertical: Boolean,
+): Offset =
+    Offset(
+        x = if (consumeHorizontal) this.x else 0f,
+        y = if (consumeVertical) this.y else 0f,
+    )
+
+private fun Velocity.consume(
+    consumeHorizontal: Boolean,
+    consumeVertical: Boolean,
+): Velocity =
+    Velocity(
+        x = if (consumeHorizontal) this.x else 0f,
+        y = if (consumeVertical) this.y else 0f,
+    )
+
+/** Scope for [HorizontalPager] content. */
+@ExperimentalPagerApi
+@Stable
+interface PagerScope {
+    /** Returns the current selected page */
+    val currentPage: Int
+
+    /** The current offset from the start of [currentPage], as a ratio of the page width. */
+    val currentPageOffset: Float
+}
+
+@ExperimentalPagerApi
+private class PagerScopeImpl(
+    private val state: PagerState,
+) : PagerScope {
+    override val currentPage: Int
+        get() = state.currentPage
+    override val currentPageOffset: Float
+        get() = state.currentPageOffset
+}
+
+/**
+ * Calculate the offset for the given [page] from the current scroll position. This is useful when
+ * using the scroll position to apply effects or animations to items.
+ *
+ * The returned offset can positive or negative, depending on whether which direction the [page] is
+ * compared to the current scroll position.
+ *
+ * @sample com.google.accompanist.sample.pager.HorizontalPagerWithOffsetTransition
+ */
+@ExperimentalPagerApi
+fun PagerScope.calculateCurrentOffsetForPage(page: Int): Float {
+    return (currentPage + currentPageOffset) - page
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/layout/pager/PagerState.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/layout/pager/PagerState.kt
new file mode 100644
index 0000000..288c26e
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/layout/pager/PagerState.kt
@@ -0,0 +1,348 @@
+/*
+ * 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.compose.layout.pager
+
+import androidx.annotation.FloatRange
+import androidx.annotation.IntRange
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.spring
+import androidx.compose.foundation.MutatePriority
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.gestures.ScrollableState
+import androidx.compose.foundation.interaction.InteractionSource
+import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.Saver
+import androidx.compose.runtime.saveable.listSaver
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import kotlin.math.absoluteValue
+import kotlin.math.roundToInt
+
+@Deprecated(
+    "Replaced with rememberPagerState(initialPage) and count parameter on Pager composables",
+    ReplaceWith("rememberPagerState(initialPage)"),
+    level = DeprecationLevel.ERROR,
+)
+@Suppress("UNUSED_PARAMETER", "NOTHING_TO_INLINE")
+@ExperimentalPagerApi
+@Composable
+inline fun rememberPagerState(
+    @IntRange(from = 0) pageCount: Int,
+    @IntRange(from = 0) initialPage: Int = 0,
+    @FloatRange(from = 0.0, to = 1.0) initialPageOffset: Float = 0f,
+    @IntRange(from = 1) initialOffscreenLimit: Int = 1,
+    infiniteLoop: Boolean = false
+): PagerState {
+    return rememberPagerState(initialPage = initialPage)
+}
+
+/**
+ * Creates a [PagerState] that is remembered across compositions.
+ *
+ * Changes to the provided values for [initialPage] will **not** result in the state being recreated
+ * or changed in any way if it has already been created.
+ *
+ * @param initialPage the initial value for [PagerState.currentPage]
+ */
+@ExperimentalPagerApi
+@Composable
+fun rememberPagerState(
+    @IntRange(from = 0) initialPage: Int = 0,
+): PagerState =
+    rememberSaveable(saver = PagerState.Saver) {
+        PagerState(
+            currentPage = initialPage,
+        )
+    }
+
+/**
+ * A state object that can be hoisted to control and observe scrolling for [HorizontalPager].
+ *
+ * In most cases, this will be created via [rememberPagerState].
+ *
+ * @param currentPage the initial value for [PagerState.currentPage]
+ */
+@ExperimentalPagerApi
+@Stable
+class PagerState(
+    @IntRange(from = 0) currentPage: Int = 0,
+) : ScrollableState {
+    // Should this be public?
+    internal val lazyListState = LazyListState(firstVisibleItemIndex = currentPage)
+
+    private var _currentPage by mutableStateOf(currentPage)
+
+    private val currentLayoutPageInfo: LazyListItemInfo?
+        get() =
+            lazyListState.layoutInfo.visibleItemsInfo
+                .asSequence()
+                .filter { it.offset <= 0 && it.offset + it.size > 0 }
+                .lastOrNull()
+
+    private val currentLayoutPageOffset: Float
+        get() =
+            currentLayoutPageInfo?.let { current ->
+                // We coerce since itemSpacing can make the offset > 1f.
+                // We don't want to count spacing in the offset so cap it to 1f
+                (-current.offset / current.size.toFloat()).coerceIn(0f, 1f)
+            }
+                ?: 0f
+
+    /**
+     * [InteractionSource] that will be used to dispatch drag events when this list is being
+     * dragged. If you want to know whether the fling (or animated scroll) is in progress, use
+     * [isScrollInProgress].
+     */
+    val interactionSource: InteractionSource
+        get() = lazyListState.interactionSource
+
+    /** The number of pages to display. */
+    @get:IntRange(from = 0)
+    val pageCount: Int by derivedStateOf { lazyListState.layoutInfo.totalItemsCount }
+
+    /**
+     * The index of the currently selected page. This may not be the page which is currently
+     * displayed on screen.
+     *
+     * To update the scroll position, use [scrollToPage] or [animateScrollToPage].
+     */
+    @get:IntRange(from = 0)
+    var currentPage: Int
+        get() = _currentPage
+        internal set(value) {
+            if (value != _currentPage) {
+                _currentPage = value
+            }
+        }
+
+    /**
+     * The current offset from the start of [currentPage], as a ratio of the page width.
+     *
+     * To update the scroll position, use [scrollToPage] or [animateScrollToPage].
+     */
+    val currentPageOffset: Float by derivedStateOf {
+        currentLayoutPageInfo?.let {
+            // The current page offset is the current layout page delta from `currentPage`
+            // (which is only updated after a scroll/animation).
+            // We calculate this by looking at the current layout page + it's offset,
+            // then subtracting the 'current page'.
+            it.index + currentLayoutPageOffset - _currentPage
+        }
+            ?: 0f
+    }
+
+    /** The target page for any on-going animations. */
+    private var animationTargetPage: Int? by mutableStateOf(null)
+
+    internal var flingAnimationTarget: (() -> Int?)? by mutableStateOf(null)
+
+    /**
+     * The target page for any on-going animations or scrolls by the user. Returns the current page
+     * if a scroll or animation is not currently in progress.
+     */
+    val targetPage: Int
+        get() =
+            animationTargetPage
+                ?: flingAnimationTarget?.invoke()
+                    ?: when {
+                    // If a scroll isn't in progress, return the current page
+                    !isScrollInProgress -> currentPage
+                    // If the offset is 0f (or very close), return the current page
+                    currentPageOffset.absoluteValue < 0.001f -> currentPage
+                    // If we're offset towards the start, guess the previous page
+                    currentPageOffset < -0.5f -> (currentPage - 1).coerceAtLeast(0)
+                    // If we're offset towards the end, guess the next page
+                    else -> (currentPage + 1).coerceAtMost(pageCount - 1)
+                }
+
+    @Deprecated(
+        "Replaced with animateScrollToPage(page, pageOffset)",
+        ReplaceWith("animateScrollToPage(page = page, pageOffset = pageOffset)")
+    )
+    @Suppress("UNUSED_PARAMETER")
+    suspend fun animateScrollToPage(
+        @IntRange(from = 0) page: Int,
+        @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
+        animationSpec: AnimationSpec<Float> = spring(),
+        initialVelocity: Float = 0f,
+        skipPages: Boolean = true,
+    ) {
+        animateScrollToPage(page = page, pageOffset = pageOffset)
+    }
+
+    /**
+     * Animate (smooth scroll) to the given page to the middle of the viewport.
+     *
+     * Cancels the currently running scroll, if any, and suspends until the cancellation is
+     * complete.
+     *
+     * @param page the page to animate to. Must be between 0 and [pageCount] (inclusive).
+     * @param pageOffset the percentage of the page width to offset, from the start of [page]. Must
+     * be in the range 0f..1f.
+     */
+    suspend fun animateScrollToPage(
+        @IntRange(from = 0) page: Int,
+        @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
+    ) {
+        requireCurrentPage(page, "page")
+        requireCurrentPageOffset(pageOffset, "pageOffset")
+        try {
+            animationTargetPage = page
+
+            if (pageOffset <= 0.005f) {
+                // If the offset is (close to) zero, just call animateScrollToItem and we're done
+                lazyListState.animateScrollToItem(index = page)
+            } else {
+                // Else we need to figure out what the offset is in pixels...
+
+                var target =
+                    lazyListState.layoutInfo.visibleItemsInfo.firstOrNull { it.index == page }
+
+                if (target != null) {
+                    // If we have access to the target page layout, we can calculate the pixel
+                    // offset from the size
+                    lazyListState.animateScrollToItem(
+                        index = page,
+                        scrollOffset = (target.size * pageOffset).roundToInt()
+                    )
+                } else {
+                    // If we don't, we use the current page size as a guide
+                    val currentSize = currentLayoutPageInfo!!.size
+                    lazyListState.animateScrollToItem(
+                        index = page,
+                        scrollOffset = (currentSize * pageOffset).roundToInt()
+                    )
+
+                    // The target should be visible now
+                    target = lazyListState.layoutInfo.visibleItemsInfo.first { it.index == page }
+
+                    if (target.size != currentSize) {
+                        // If the size we used for calculating the offset differs from the actual
+                        // target page size, we need to scroll again. This doesn't look great,
+                        // but there's not much else we can do.
+                        lazyListState.animateScrollToItem(
+                            index = page,
+                            scrollOffset = (target.size * pageOffset).roundToInt()
+                        )
+                    }
+                }
+            }
+        } finally {
+            // We need to manually call this, as the `animateScrollToItem` call above will happen
+            // in 1 frame, which is usually too fast for the LaunchedEffect in Pager to detect
+            // the change. This is especially true when running unit tests.
+            onScrollFinished()
+        }
+    }
+
+    /**
+     * Instantly brings the item at [page] to the middle of the viewport.
+     *
+     * Cancels the currently running scroll, if any, and suspends until the cancellation is
+     * complete.
+     *
+     * @param page the page to snap to. Must be between 0 and [pageCount] (inclusive).
+     */
+    suspend fun scrollToPage(
+        @IntRange(from = 0) page: Int,
+        @FloatRange(from = 0.0, to = 1.0) pageOffset: Float = 0f,
+    ) {
+        requireCurrentPage(page, "page")
+        requireCurrentPageOffset(pageOffset, "pageOffset")
+        try {
+            animationTargetPage = page
+
+            // First scroll to the given page. It will now be laid out at offset 0
+            lazyListState.scrollToItem(index = page)
+
+            // If we have a start spacing, we need to offset (scroll) by that too
+            if (pageOffset > 0.0001f) {
+                scroll { currentLayoutPageInfo?.let { scrollBy(it.size * pageOffset) } }
+            }
+        } finally {
+            // We need to manually call this, as the `scroll` call above will happen in 1 frame,
+            // which is usually too fast for the LaunchedEffect in Pager to detect the change.
+            // This is especially true when running unit tests.
+            onScrollFinished()
+        }
+    }
+
+    internal fun onScrollFinished() {
+        // Then update the current page to our layout page
+        currentPage = currentLayoutPageInfo?.index ?: 0
+        // Clear the animation target page
+        animationTargetPage = null
+    }
+
+    override suspend fun scroll(
+        scrollPriority: MutatePriority,
+        block: suspend ScrollScope.() -> Unit
+    ) = lazyListState.scroll(scrollPriority, block)
+
+    override fun dispatchRawDelta(delta: Float): Float {
+        return lazyListState.dispatchRawDelta(delta)
+    }
+
+    override val isScrollInProgress: Boolean
+        get() = lazyListState.isScrollInProgress
+
+    override fun toString(): String =
+        "PagerState(" +
+            "pageCount=$pageCount, " +
+            "currentPage=$currentPage, " +
+            "currentPageOffset=$currentPageOffset" +
+            ")"
+
+    private fun requireCurrentPage(value: Int, name: String) {
+        if (pageCount == 0) {
+            require(value == 0) { "$name must be 0 when pageCount is 0" }
+        } else {
+            require(value in 0 until pageCount) { "$name[$value] must be >= 0 and < pageCount" }
+        }
+    }
+
+    private fun requireCurrentPageOffset(value: Float, name: String) {
+        if (pageCount == 0) {
+            require(value == 0f) { "$name must be 0f when pageCount is 0" }
+        } else {
+            require(value in 0f..1f) { "$name must be >= 0 and <= 1" }
+        }
+    }
+
+    companion object {
+        /** The default [Saver] implementation for [PagerState]. */
+        val Saver: Saver<PagerState, *> =
+            listSaver(
+                save = {
+                    listOf<Any>(
+                        it.currentPage,
+                    )
+                },
+                restore = {
+                    PagerState(
+                        currentPage = it[0] as Int,
+                    )
+                }
+            )
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/layout/pager/SnappingFlingBehavior.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/layout/pager/SnappingFlingBehavior.kt
new file mode 100644
index 0000000..0b53f532
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/layout/pager/SnappingFlingBehavior.kt
@@ -0,0 +1,270 @@
+/*
+ * 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.compose.layout.pager
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.AnimationState
+import androidx.compose.animation.core.DecayAnimationSpec
+import androidx.compose.animation.core.animateDecay
+import androidx.compose.animation.core.animateTo
+import androidx.compose.animation.core.calculateTargetValue
+import androidx.compose.animation.core.spring
+import androidx.compose.animation.rememberSplineBasedDecay
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ScrollScope
+import androidx.compose.foundation.lazy.LazyListItemInfo
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import kotlin.math.abs
+
+/** Default values used for [SnappingFlingBehavior] & [rememberSnappingFlingBehavior]. */
+internal object SnappingFlingBehaviorDefaults {
+    /** TODO */
+    val snapAnimationSpec: AnimationSpec<Float> = spring(stiffness = 600f)
+}
+
+/**
+ * Create and remember a snapping [FlingBehavior] to be used with [LazyListState].
+ *
+ * TODO: move this to a new module and make it public
+ *
+ * @param lazyListState The [LazyListState] to update.
+ * @param decayAnimationSpec The decay animation spec to use for decayed flings.
+ * @param snapAnimationSpec The animation spec to use when snapping.
+ */
+@Composable
+internal fun rememberSnappingFlingBehavior(
+    lazyListState: LazyListState,
+    decayAnimationSpec: DecayAnimationSpec<Float> = rememberSplineBasedDecay(),
+    snapAnimationSpec: AnimationSpec<Float> = SnappingFlingBehaviorDefaults.snapAnimationSpec,
+): SnappingFlingBehavior =
+    remember(lazyListState, decayAnimationSpec, snapAnimationSpec) {
+        SnappingFlingBehavior(
+            lazyListState = lazyListState,
+            decayAnimationSpec = decayAnimationSpec,
+            snapAnimationSpec = snapAnimationSpec,
+        )
+    }
+
+/**
+ * A snapping [FlingBehavior] for [LazyListState]. Typically this would be created via
+ * [rememberSnappingFlingBehavior].
+ *
+ * @param lazyListState The [LazyListState] to update.
+ * @param decayAnimationSpec The decay animation spec to use for decayed flings.
+ * @param snapAnimationSpec The animation spec to use when snapping.
+ */
+internal class SnappingFlingBehavior(
+    private val lazyListState: LazyListState,
+    private val decayAnimationSpec: DecayAnimationSpec<Float>,
+    private val snapAnimationSpec: AnimationSpec<Float>,
+) : FlingBehavior {
+    /** The target item index for any on-going animations. */
+    var animationTarget: Int? by mutableStateOf(null)
+        private set
+
+    override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
+        val itemInfo = currentItemInfo ?: return initialVelocity
+
+        // If the decay fling can scroll past the current item, fling with decay
+        return if (decayAnimationSpec.canFlingPastCurrentItem(itemInfo, initialVelocity)) {
+            performDecayFling(initialVelocity, itemInfo)
+        } else {
+            // Otherwise we 'spring' to current/next item
+            performSpringFling(
+                index =
+                    when {
+                        // If the velocity is greater than 1 item per second (velocity is px/s),
+                        // spring
+                        // in the relevant direction
+                        initialVelocity > itemInfo.size -> {
+                            (itemInfo.index + 1).coerceAtMost(
+                                lazyListState.layoutInfo.totalItemsCount - 1
+                            )
+                        }
+                        initialVelocity < -itemInfo.size -> itemInfo.index
+                        // If the velocity is 0 (or less than the size of the item), spring to
+                        // whichever item is closest to the snap point
+                        itemInfo.offset < -itemInfo.size / 2 -> itemInfo.index + 1
+                        else -> itemInfo.index
+                    },
+                initialVelocity = initialVelocity,
+            )
+        }
+    }
+
+    private suspend fun ScrollScope.performDecayFling(
+        initialVelocity: Float,
+        startItem: LazyListItemInfo,
+    ): Float {
+        val index =
+            when {
+                initialVelocity > 0 -> startItem.index + 1
+                else -> startItem.index
+            }
+        val forward = index > (currentItemInfo?.index ?: return initialVelocity)
+
+        // Update the animationTarget
+        animationTarget = index
+
+        var velocityLeft = initialVelocity
+        var lastValue = 0f
+        AnimationState(
+                initialValue = 0f,
+                initialVelocity = initialVelocity,
+            )
+            .animateDecay(decayAnimationSpec) {
+                val delta = value - lastValue
+                val consumed = scrollBy(delta)
+                lastValue = value
+                velocityLeft = this.velocity
+
+                val current = currentItemInfo
+                if (current == null) {
+                    cancelAnimation()
+                    return@animateDecay
+                }
+
+                if (
+                    !forward &&
+                        (current.index < index || current.index == index && current.offset >= 0)
+                ) {
+                    // 'snap back' to the item as we may have scrolled past it
+                    scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
+                    cancelAnimation()
+                } else if (
+                    forward &&
+                        (current.index > index || current.index == index && current.offset <= 0)
+                ) {
+                    // 'snap back' to the item as we may have scrolled past it
+                    scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
+                    cancelAnimation()
+                } else if (abs(delta - consumed) > 0.5f) {
+                    // avoid rounding errors and stop if anything is unconsumed
+                    cancelAnimation()
+                }
+            }
+        animationTarget = null
+        return velocityLeft
+    }
+
+    private suspend fun ScrollScope.performSpringFling(
+        index: Int,
+        scrollOffset: Int = 0,
+        initialVelocity: Float = 0f,
+    ): Float {
+        // If we don't have a current layout, we can't snap
+        val initialItem = currentItemInfo ?: return initialVelocity
+
+        val forward = index > initialItem.index
+        // We add 10% on to the size of the current item, to compensate for any item spacing, etc
+        val target = (if (forward) initialItem.size else -initialItem.size) * 1.1f
+
+        // Update the animationTarget
+        animationTarget = index
+
+        var velocityLeft = initialVelocity
+        var lastValue = 0f
+        AnimationState(
+                initialValue = 0f,
+                initialVelocity = initialVelocity,
+            )
+            .animateTo(
+                targetValue = target,
+                animationSpec = snapAnimationSpec,
+            ) {
+                // Springs can overshoot their target, clamp to the desired range
+                val coercedValue =
+                    if (forward) {
+                        value.coerceAtMost(target)
+                    } else {
+                        value.coerceAtLeast(target)
+                    }
+                val delta = coercedValue - lastValue
+                val consumed = scrollBy(delta)
+                lastValue = coercedValue
+                velocityLeft = this.velocity
+
+                val current = currentItemInfo
+                if (current == null) {
+                    cancelAnimation()
+                    return@animateTo
+                }
+
+                if (scrolledPastItem(initialVelocity, current, index, scrollOffset)) {
+                    // If we've scrolled to/past the item, stop the animation. We may also need to
+                    // 'snap back' to the item as we may have scrolled past it
+                    scrollBy(lazyListState.calculateScrollOffsetToItem(index).toFloat())
+                    cancelAnimation()
+                } else if (abs(delta - consumed) > 0.5f) {
+                    // avoid rounding errors and stop if anything is unconsumed
+                    cancelAnimation()
+                }
+            }
+        animationTarget = null
+        return velocityLeft
+    }
+
+    private fun LazyListState.calculateScrollOffsetToItem(index: Int): Int {
+        return layoutInfo.visibleItemsInfo.firstOrNull { it.index == index }?.offset ?: 0
+    }
+
+    private val currentItemInfo: LazyListItemInfo?
+        get() =
+            lazyListState.layoutInfo.visibleItemsInfo
+                .asSequence()
+                .filter { it.offset <= 0 && it.offset + it.size > 0 }
+                .lastOrNull()
+}
+
+private fun scrolledPastItem(
+    initialVelocity: Float,
+    currentItem: LazyListItemInfo,
+    targetIndex: Int,
+    targetScrollOffset: Int = 0,
+): Boolean {
+    return if (initialVelocity > 0) {
+        // forward
+        currentItem.index > targetIndex ||
+            (currentItem.index == targetIndex && currentItem.offset <= targetScrollOffset)
+    } else {
+        // backwards
+        currentItem.index < targetIndex ||
+            (currentItem.index == targetIndex && currentItem.offset >= targetScrollOffset)
+    }
+}
+
+private fun DecayAnimationSpec<Float>.canFlingPastCurrentItem(
+    currentItem: LazyListItemInfo,
+    initialVelocity: Float,
+): Boolean {
+    val targetValue =
+        calculateTargetValue(
+            initialValue = currentItem.offset.toFloat(),
+            initialVelocity = initialVelocity,
+        )
+    return when {
+        // forward. We add 10% onto the size to cater for any item spacing
+        initialVelocity > 0 -> targetValue <= -(currentItem.size * 1.1f)
+        // backwards. We add 10% onto the size to cater for any item spacing
+        else -> targetValue >= (currentItem.size * 0.1f)
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/modifiers/Padding.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/modifiers/Padding.kt
new file mode 100644
index 0000000..3b13c0b
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/modifiers/Padding.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.compose.modifiers
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+import androidx.compose.ui.unit.offset
+
+// This file was mostly copy/pasted from by androidx.compose.foundation.layout.Padding.kt and
+// contains modifiers with lambda parameters to change the padding of a Composable without
+// triggering recomposition when the paddings change.
+//
+// These should be used instead of the traditional size modifiers when the size changes often, for
+// instance when it is animated.
+//
+// TODO(b/247473910): Remove these modifiers once they can be fully replaced by layout animations
+// APIs.
+
+/** @see androidx.compose.foundation.layout.padding */
+fun Modifier.padding(
+    start: Density.() -> Int = PaddingUnspecified,
+    top: Density.() -> Int = PaddingUnspecified,
+    end: Density.() -> Int = PaddingUnspecified,
+    bottom: Density.() -> Int = PaddingUnspecified,
+) =
+    this.then(
+        PaddingModifier(
+            start,
+            top,
+            end,
+            bottom,
+            rtlAware = true,
+            inspectorInfo =
+                debugInspectorInfo {
+                    name = "padding"
+                    properties["start"] = start
+                    properties["top"] = top
+                    properties["end"] = end
+                    properties["bottom"] = bottom
+                }
+        )
+    )
+
+/** @see androidx.compose.foundation.layout.padding */
+fun Modifier.padding(
+    horizontal: Density.() -> Int = PaddingUnspecified,
+    vertical: Density.() -> Int = PaddingUnspecified,
+): Modifier {
+    return this.then(
+        PaddingModifier(
+            start = horizontal,
+            top = vertical,
+            end = horizontal,
+            bottom = vertical,
+            rtlAware = true,
+            inspectorInfo =
+                debugInspectorInfo {
+                    name = "padding"
+                    properties["horizontal"] = horizontal
+                    properties["vertical"] = vertical
+                }
+        )
+    )
+}
+
+private val PaddingUnspecified: Density.() -> Int = { 0 }
+
+private class PaddingModifier(
+    val start: Density.() -> Int,
+    val top: Density.() -> Int,
+    val end: Density.() -> Int,
+    val bottom: Density.() -> Int,
+    val rtlAware: Boolean,
+    inspectorInfo: InspectorInfo.() -> Unit
+) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val start = start()
+        val top = top()
+        val end = end()
+        val bottom = bottom()
+
+        val horizontal = start + end
+        val vertical = top + bottom
+
+        val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))
+
+        val width = constraints.constrainWidth(placeable.width + horizontal)
+        val height = constraints.constrainHeight(placeable.height + vertical)
+        return layout(width, height) {
+            if (rtlAware) {
+                placeable.placeRelative(start, top)
+            } else {
+                placeable.place(start, top)
+            }
+        }
+    }
+
+    override fun hashCode(): Int {
+        var result = start.hashCode()
+        result = 31 * result + top.hashCode()
+        result = 31 * result + end.hashCode()
+        result = 31 * result + bottom.hashCode()
+        result = 31 * result + rtlAware.hashCode()
+        return result
+    }
+
+    override fun equals(other: Any?): Boolean {
+        val otherModifier = other as? PaddingModifier ?: return false
+        return start == otherModifier.start &&
+            top == otherModifier.top &&
+            end == otherModifier.end &&
+            bottom == otherModifier.bottom &&
+            rtlAware == otherModifier.rtlAware
+    }
+}
diff --git a/packages/SystemUI/compose/core/src/com/android/systemui/compose/modifiers/Size.kt b/packages/SystemUI/compose/core/src/com/android/systemui/compose/modifiers/Size.kt
new file mode 100644
index 0000000..570d2431
--- /dev/null
+++ b/packages/SystemUI/compose/core/src/com/android/systemui/compose/modifiers/Size.kt
@@ -0,0 +1,247 @@
+/*
+ * 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.compose.modifiers
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.IntrinsicMeasurable
+import androidx.compose.ui.layout.IntrinsicMeasureScope
+import androidx.compose.ui.layout.LayoutModifier
+import androidx.compose.ui.layout.Measurable
+import androidx.compose.ui.layout.MeasureResult
+import androidx.compose.ui.layout.MeasureScope
+import androidx.compose.ui.platform.InspectorInfo
+import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.platform.debugInspectorInfo
+import androidx.compose.ui.unit.Constraints
+import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.constrain
+import androidx.compose.ui.unit.constrainHeight
+import androidx.compose.ui.unit.constrainWidth
+
+// This file was mostly copy pasted from androidx.compose.foundation.layout.Size.kt and contains
+// modifiers with lambda parameters to change the (min/max) size of a Composable without triggering
+// recomposition when the sizes change.
+//
+// These should be used instead of the traditional size modifiers when the size changes often, for
+// instance when it is animated.
+//
+// TODO(b/247473910): Remove these modifiers once they can be fully replaced by layout animations
+// APIs.
+
+/** @see androidx.compose.foundation.layout.width */
+fun Modifier.width(width: Density.() -> Int) =
+    this.then(
+        SizeModifier(
+            minWidth = width,
+            maxWidth = width,
+            enforceIncoming = true,
+            inspectorInfo =
+                debugInspectorInfo {
+                    name = "width"
+                    value = width
+                }
+        )
+    )
+
+/** @see androidx.compose.foundation.layout.height */
+fun Modifier.height(height: Density.() -> Int) =
+    this.then(
+        SizeModifier(
+            minHeight = height,
+            maxHeight = height,
+            enforceIncoming = true,
+            inspectorInfo =
+                debugInspectorInfo {
+                    name = "height"
+                    value = height
+                }
+        )
+    )
+
+/** @see androidx.compose.foundation.layout.size */
+fun Modifier.size(width: Density.() -> Int, height: Density.() -> Int) =
+    this.then(
+        SizeModifier(
+            minWidth = width,
+            maxWidth = width,
+            minHeight = height,
+            maxHeight = height,
+            enforceIncoming = true,
+            inspectorInfo =
+                debugInspectorInfo {
+                    name = "size"
+                    properties["width"] = width
+                    properties["height"] = height
+                }
+        )
+    )
+
+private val SizeUnspecified: Density.() -> Int = { 0 }
+
+private class SizeModifier(
+    private val minWidth: Density.() -> Int = SizeUnspecified,
+    private val minHeight: Density.() -> Int = SizeUnspecified,
+    private val maxWidth: Density.() -> Int = SizeUnspecified,
+    private val maxHeight: Density.() -> Int = SizeUnspecified,
+    private val enforceIncoming: Boolean,
+    inspectorInfo: InspectorInfo.() -> Unit
+) : LayoutModifier, InspectorValueInfo(inspectorInfo) {
+    private val Density.targetConstraints: Constraints
+        get() {
+            val maxWidth =
+                if (maxWidth != SizeUnspecified) {
+                    maxWidth().coerceAtLeast(0)
+                } else {
+                    Constraints.Infinity
+                }
+            val maxHeight =
+                if (maxHeight != SizeUnspecified) {
+                    maxHeight().coerceAtLeast(0)
+                } else {
+                    Constraints.Infinity
+                }
+            val minWidth =
+                if (minWidth != SizeUnspecified) {
+                    minWidth().coerceAtMost(maxWidth).coerceAtLeast(0).let {
+                        if (it != Constraints.Infinity) it else 0
+                    }
+                } else {
+                    0
+                }
+            val minHeight =
+                if (minHeight != SizeUnspecified) {
+                    minHeight().coerceAtMost(maxHeight).coerceAtLeast(0).let {
+                        if (it != Constraints.Infinity) it else 0
+                    }
+                } else {
+                    0
+                }
+            return Constraints(
+                minWidth = minWidth,
+                minHeight = minHeight,
+                maxWidth = maxWidth,
+                maxHeight = maxHeight
+            )
+        }
+
+    override fun MeasureScope.measure(
+        measurable: Measurable,
+        constraints: Constraints
+    ): MeasureResult {
+        val wrappedConstraints =
+            targetConstraints.let { targetConstraints ->
+                if (enforceIncoming) {
+                    constraints.constrain(targetConstraints)
+                } else {
+                    val resolvedMinWidth =
+                        if (minWidth != SizeUnspecified) {
+                            targetConstraints.minWidth
+                        } else {
+                            constraints.minWidth.coerceAtMost(targetConstraints.maxWidth)
+                        }
+                    val resolvedMaxWidth =
+                        if (maxWidth != SizeUnspecified) {
+                            targetConstraints.maxWidth
+                        } else {
+                            constraints.maxWidth.coerceAtLeast(targetConstraints.minWidth)
+                        }
+                    val resolvedMinHeight =
+                        if (minHeight != SizeUnspecified) {
+                            targetConstraints.minHeight
+                        } else {
+                            constraints.minHeight.coerceAtMost(targetConstraints.maxHeight)
+                        }
+                    val resolvedMaxHeight =
+                        if (maxHeight != SizeUnspecified) {
+                            targetConstraints.maxHeight
+                        } else {
+                            constraints.maxHeight.coerceAtLeast(targetConstraints.minHeight)
+                        }
+                    Constraints(
+                        resolvedMinWidth,
+                        resolvedMaxWidth,
+                        resolvedMinHeight,
+                        resolvedMaxHeight
+                    )
+                }
+            }
+        val placeable = measurable.measure(wrappedConstraints)
+        return layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) }
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int {
+        val constraints = targetConstraints
+        return if (constraints.hasFixedWidth) {
+            constraints.maxWidth
+        } else {
+            constraints.constrainWidth(measurable.minIntrinsicWidth(height))
+        }
+    }
+
+    override fun IntrinsicMeasureScope.minIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int {
+        val constraints = targetConstraints
+        return if (constraints.hasFixedHeight) {
+            constraints.maxHeight
+        } else {
+            constraints.constrainHeight(measurable.minIntrinsicHeight(width))
+        }
+    }
+
+    override fun IntrinsicMeasureScope.maxIntrinsicWidth(
+        measurable: IntrinsicMeasurable,
+        height: Int
+    ): Int {
+        val constraints = targetConstraints
+        return if (constraints.hasFixedWidth) {
+            constraints.maxWidth
+        } else {
+            constraints.constrainWidth(measurable.maxIntrinsicWidth(height))
+        }
+    }
+
+    override fun IntrinsicMeasureScope.maxIntrinsicHeight(
+        measurable: IntrinsicMeasurable,
+        width: Int
+    ): Int {
+        val constraints = targetConstraints
+        return if (constraints.hasFixedHeight) {
+            constraints.maxHeight
+        } else {
+            constraints.constrainHeight(measurable.maxIntrinsicHeight(width))
+        }
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other !is SizeModifier) return false
+        return minWidth == other.minWidth &&
+            minHeight == other.minHeight &&
+            maxWidth == other.maxWidth &&
+            maxHeight == other.maxHeight &&
+            enforceIncoming == other.enforceIncoming
+    }
+
+    override fun hashCode() =
+        (((((minWidth.hashCode() * 31 + minHeight.hashCode()) * 31) + maxWidth.hashCode()) * 31) +
+            maxHeight.hashCode()) * 31
+}
diff --git a/packages/SystemUI/compose/gallery/Android.bp b/packages/SystemUI/compose/gallery/Android.bp
deleted file mode 100644
index 5a7a1e1..0000000
--- a/packages/SystemUI/compose/gallery/Android.bp
+++ /dev/null
@@ -1,86 +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 {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
-}
-
-android_library {
-    name: "SystemUIComposeGalleryLib",
-    manifest: "AndroidManifest.xml",
-
-    srcs: [
-        "src/**/*.kt",
-        ":SystemUI-tests-utils",
-    ],
-
-    resource_dirs: [
-        "res",
-    ],
-
-    static_libs: [
-        "SystemUI-core",
-        "SystemUIComposeCore",
-        "SystemUIComposeFeatures",
-
-        "androidx.compose.runtime_runtime",
-        "androidx.compose.material3_material3",
-        "androidx.compose.material_material-icons-extended",
-        "androidx.activity_activity-compose",
-        "androidx.navigation_navigation-compose",
-
-        "androidx.appcompat_appcompat",
-
-        // TODO(b/240431193): Remove the dependencies and depend on
-        // SystemUI-test-utils directly.
-        "androidx.test.runner",
-        "mockito-target-extended-minus-junit4",
-        "testables",
-        "truth-prebuilt",
-        "androidx.test.uiautomator",
-        "kotlinx_coroutines_test",
-    ],
-
-    libs: [
-        "android.test.mock",
-    ],
-
-    kotlincflags: ["-Xjvm-default=all"],
-}
-
-android_app {
-    name: "SystemUIComposeGallery",
-    defaults: ["platform_app_defaults"],
-    manifest: "app/AndroidManifest.xml",
-
-    static_libs: [
-        "SystemUIComposeGalleryLib",
-    ],
-
-    platform_apis: true,
-    system_ext_specific: true,
-    certificate: "platform",
-    privileged: true,
-
-    optimize: {
-        proguard_flags_files: ["proguard-rules.pro"],
-    },
-
-    dxflags: ["--multi-dex"],
-}
diff --git a/packages/SystemUI/compose/gallery/AndroidManifest.xml b/packages/SystemUI/compose/gallery/AndroidManifest.xml
deleted file mode 100644
index 2f30651..0000000
--- a/packages/SystemUI/compose/gallery/AndroidManifest.xml
+++ /dev/null
@@ -1,55 +0,0 @@
-<?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"
-    xmlns:tools="http://schemas.android.com/tools"
-    package="com.android.systemui.compose.gallery">
-    <!-- To emulate a display size and density. -->
-    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
-
-    <application
-        android:name="android.app.Application"
-        android:appComponentFactory="androidx.core.app.AppComponentFactory"
-        tools:replace="android:name,android:appComponentFactory">
-        <!-- Disable providers from SystemUI -->
-        <provider android:name="com.android.systemui.keyguard.KeyguardSliceProvider"
-            android:authorities="com.android.systemui.test.keyguard.disabled"
-            android:enabled="false"
-            tools:replace="android:authorities"
-            tools:node="remove" />
-        <provider android:name="com.google.android.systemui.keyguard.KeyguardSliceProviderGoogle"
-            android:authorities="com.android.systemui.test.keyguard.disabled"
-            android:enabled="false"
-            tools:replace="android:authorities"
-            tools:node="remove" />
-        <provider android:name="com.android.keyguard.clock.ClockOptionsProvider"
-            android:authorities="com.android.systemui.test.keyguard.clock.disabled"
-            android:enabled="false"
-            tools:replace="android:authorities"
-            tools:node="remove" />
-        <provider android:name="com.android.systemui.people.PeopleProvider"
-            android:authorities="com.android.systemui.test.people.disabled"
-            android:enabled="false"
-            tools:replace="android:authorities"
-            tools:node="remove" />
-        <provider android:name="androidx.core.content.FileProvider"
-            android:authorities="com.android.systemui.test.fileprovider.disabled"
-            android:enabled="false"
-            tools:replace="android:authorities"
-            tools:node="remove"/>
-    </application>
-</manifest>
diff --git a/packages/SystemUI/compose/gallery/TEST_MAPPING b/packages/SystemUI/compose/gallery/TEST_MAPPING
deleted file mode 100644
index c7f8a92..0000000
--- a/packages/SystemUI/compose/gallery/TEST_MAPPING
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "presubmit": [
-    {
-      "name": "SystemUIComposeGalleryTests",
-      "options": [
-        {
-          "exclude-annotation": "org.junit.Ignore"
-        },
-        {
-          "exclude-annotation": "androidx.test.filters.FlakyTest"
-        }
-      ]
-    }
-  ]
-}
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/app/AndroidManifest.xml b/packages/SystemUI/compose/gallery/app/AndroidManifest.xml
deleted file mode 100644
index 1f3fd8c..0000000
--- a/packages/SystemUI/compose/gallery/app/AndroidManifest.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?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"
-    xmlns:tools="http://schemas.android.com/tools"
-    package="com.android.systemui.compose.gallery.app">
-    <application
-        android:allowBackup="true"
-        android:icon="@mipmap/ic_launcher"
-        android:label="@string/app_name"
-        android:roundIcon="@mipmap/ic_launcher_round"
-        android:supportsRtl="true"
-        android:theme="@style/Theme.SystemUI.Gallery"
-        tools:replace="android:icon,android:theme,android:label">
-        <activity
-            android:name="com.android.systemui.compose.gallery.GalleryActivity"
-            android:exported="true"
-            android:label="@string/app_name">
-            <intent-filter>
-                <action android:name="android.intent.action.MAIN" />
-                <category android:name="android.intent.category.LAUNCHER" />
-            </intent-filter>
-        </activity>
-    </application>
-</manifest>
diff --git a/packages/SystemUI/compose/gallery/proguard-rules.pro b/packages/SystemUI/compose/gallery/proguard-rules.pro
deleted file mode 100644
index 481bb43..0000000
--- a/packages/SystemUI/compose/gallery/proguard-rules.pro
+++ /dev/null
@@ -1,21 +0,0 @@
-# Add project specific ProGuard rules here.
-# You can control the set of applied configuration files using the
-# proguardFiles setting in build.gradle.
-#
-# For more details, see
-#   http://developer.android.com/guide/developing/tools/proguard.html
-
-# If your project uses WebView with JS, uncomment the following
-# and specify the fully qualified class name to the JavaScript interface
-# class:
-#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
-#   public *;
-#}
-
-# Uncomment this to preserve the line number information for
-# debugging stack traces.
-#-keepattributes SourceFile,LineNumberTable
-
-# If you keep the line number information, uncomment this to
-# hide the original source file name.
-#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/drawable-v24/ic_launcher_foreground.xml b/packages/SystemUI/compose/gallery/res/drawable-v24/ic_launcher_foreground.xml
deleted file mode 100644
index 966abaf..0000000
--- a/packages/SystemUI/compose/gallery/res/drawable-v24/ic_launcher_foreground.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:aapt="http://schemas.android.com/aapt"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportHeight="108"
-    android:viewportWidth="108">
-  <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
-    <aapt:attr name="android:fillColor">
-      <gradient
-          android:endX="85.84757"
-          android:endY="92.4963"
-          android:startX="42.9492"
-          android:startY="49.59793"
-          android:type="linear">
-        <item
-            android:color="#44000000"
-            android:offset="0.0" />
-        <item
-            android:color="#00000000"
-            android:offset="1.0" />
-      </gradient>
-    </aapt:attr>
-  </path>
-  <path
-      android:fillColor="#FFFFFF"
-      android:fillType="nonZero"
-      android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
-      android:strokeColor="#00000000"
-      android:strokeWidth="1" />
-</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/drawable/ic_launcher_background.xml b/packages/SystemUI/compose/gallery/res/drawable/ic_launcher_background.xml
deleted file mode 100644
index 61bb79e..0000000
--- a/packages/SystemUI/compose/gallery/res/drawable/ic_launcher_background.xml
+++ /dev/null
@@ -1,170 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-    android:width="108dp"
-    android:height="108dp"
-    android:viewportHeight="108"
-    android:viewportWidth="108">
-  <path
-      android:fillColor="#3DDC84"
-      android:pathData="M0,0h108v108h-108z" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M9,0L9,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M19,0L19,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M29,0L29,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M39,0L39,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M49,0L49,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M59,0L59,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M69,0L69,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M79,0L79,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M89,0L89,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M99,0L99,108"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,9L108,9"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,19L108,19"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,29L108,29"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,39L108,39"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,49L108,49"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,59L108,59"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,69L108,69"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,79L108,79"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,89L108,89"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M0,99L108,99"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M19,29L89,29"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M19,39L89,39"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M19,49L89,49"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M19,59L89,59"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M19,69L89,69"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M19,79L89,79"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M29,19L29,89"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M39,19L39,89"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M49,19L49,89"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M59,19L59,89"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M69,19L69,89"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-  <path
-      android:fillColor="#00000000"
-      android:pathData="M79,19L79,89"
-      android:strokeColor="#33FFFFFF"
-      android:strokeWidth="0.8" />
-</vector>
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten1.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten1.jpeg
deleted file mode 100644
index 6241b0b..0000000
--- a/packages/SystemUI/compose/gallery/res/drawable/kitten1.jpeg
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten2.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten2.jpeg
deleted file mode 100644
index 870ef13..0000000
--- a/packages/SystemUI/compose/gallery/res/drawable/kitten2.jpeg
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten3.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten3.jpeg
deleted file mode 100644
index bb7261c..0000000
--- a/packages/SystemUI/compose/gallery/res/drawable/kitten3.jpeg
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten4.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten4.jpeg
deleted file mode 100644
index e34b7dd..0000000
--- a/packages/SystemUI/compose/gallery/res/drawable/kitten4.jpeg
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten5.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten5.jpeg
deleted file mode 100644
index 9cde24b..0000000
--- a/packages/SystemUI/compose/gallery/res/drawable/kitten5.jpeg
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/drawable/kitten6.jpeg b/packages/SystemUI/compose/gallery/res/drawable/kitten6.jpeg
deleted file mode 100644
index 17825b6..0000000
--- a/packages/SystemUI/compose/gallery/res/drawable/kitten6.jpeg
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher.xml b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher.xml
deleted file mode 100644
index 03eed25..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-  <background android:drawable="@drawable/ic_launcher_background" />
-  <foreground android:drawable="@drawable/ic_launcher_foreground" />
-</adaptive-icon>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher_round.xml b/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher_round.xml
deleted file mode 100644
index 03eed25..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-  <background android:drawable="@drawable/ic_launcher_background" />
-  <foreground android:drawable="@drawable/ic_launcher_foreground" />
-</adaptive-icon>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher.webp
deleted file mode 100644
index c209e78..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher_round.webp
deleted file mode 100644
index b2dfe3d..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-hdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher.webp
deleted file mode 100644
index 4f0f1d6..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher_round.webp
deleted file mode 100644
index 62b611d..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-mdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher.webp
deleted file mode 100644
index 948a307..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher_round.webp
deleted file mode 100644
index 1b9a695..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-xhdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher.webp
deleted file mode 100644
index 28d4b77..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9287f508..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-xxhdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher.webp
deleted file mode 100644
index aa7d642..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher_round.webp b/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher_round.webp
deleted file mode 100644
index 9126ae3..0000000
--- a/packages/SystemUI/compose/gallery/res/mipmap-xxxhdpi/ic_launcher_round.webp
+++ /dev/null
Binary files differ
diff --git a/packages/SystemUI/compose/gallery/res/values/colors.xml b/packages/SystemUI/compose/gallery/res/values/colors.xml
deleted file mode 100644
index a2fcbff..0000000
--- a/packages/SystemUI/compose/gallery/res/values/colors.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<resources>
-    <color name="ic_launcher_background">#FFFFFF</color>
-</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/values/strings.xml b/packages/SystemUI/compose/gallery/res/values/strings.xml
deleted file mode 100644
index 86bdb05..0000000
--- a/packages/SystemUI/compose/gallery/res/values/strings.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<resources>
-    <!-- Application name [CHAR LIMIT=NONE] -->
-    <string name="app_name">SystemUI Gallery</string>
-</resources>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/res/values/themes.xml b/packages/SystemUI/compose/gallery/res/values/themes.xml
deleted file mode 100644
index 45fa1f5d..0000000
--- a/packages/SystemUI/compose/gallery/res/values/themes.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-     Copyright (C) 2022 The Android Open Source Project
-
-     Licensed under the Apache License, Version 2.0 (the "License");
-     you may not use this file except in compliance with the License.
-     You may obtain a copy of the License at
-
-          http://www.apache.org/licenses/LICENSE-2.0
-
-     Unless required by applicable law or agreed to in writing, software
-     distributed under the License is distributed on an "AS IS" BASIS,
-     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-     See the License for the specific language governing permissions and
-     limitations under the License.
--->
-<resources xmlns:tools="http://schemas.android.com/tools">
-    <style name="Theme.SystemUI.Gallery">
-        <item name="android:windowActionBar">false</item>
-        <item name="android:windowNoTitle">true</item>
-
-        <item name="android:statusBarColor" tools:targetApi="l">
-            @android:color/transparent
-        </item>
-        <item name="android:navigationBarColor" tools:targetApi="l">
-            @android:color/transparent
-        </item>
-        <item name="android:windowLightStatusBar">true</item>
-    </style>
-</resources>
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ButtonsScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ButtonsScreen.kt
deleted file mode 100644
index 881a1def..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ButtonsScreen.kt
+++ /dev/null
@@ -1,77 +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.
- *
- */
-
-@file:OptIn(ExperimentalMaterial3Api::class)
-
-package com.android.systemui.compose.gallery
-
-import androidx.compose.foundation.layout.Column
-import androidx.compose.material3.ExperimentalMaterial3Api
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.android.systemui.compose.SysUiButton
-import com.android.systemui.compose.SysUiOutlinedButton
-import com.android.systemui.compose.SysUiTextButton
-
-@Composable
-fun ButtonsScreen(
-    modifier: Modifier = Modifier,
-) {
-    Column(
-        modifier = modifier,
-    ) {
-        SysUiButton(
-            onClick = {},
-        ) {
-            Text("SysUiButton")
-        }
-
-        SysUiButton(
-            onClick = {},
-            enabled = false,
-        ) {
-            Text("SysUiButton - disabled")
-        }
-
-        SysUiOutlinedButton(
-            onClick = {},
-        ) {
-            Text("SysUiOutlinedButton")
-        }
-
-        SysUiOutlinedButton(
-            onClick = {},
-            enabled = false,
-        ) {
-            Text("SysUiOutlinedButton - disabled")
-        }
-
-        SysUiTextButton(
-            onClick = {},
-        ) {
-            Text("SysUiTextButton")
-        }
-
-        SysUiTextButton(
-            onClick = {},
-            enabled = false,
-        ) {
-            Text("SysUiTextButton - disabled")
-        }
-    }
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ColorsScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ColorsScreen.kt
deleted file mode 100644
index dfa1b26..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ColorsScreen.kt
+++ /dev/null
@@ -1,139 +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.compose.gallery
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import com.android.systemui.compose.theme.LocalAndroidColorScheme
-
-/** The screen that shows all the Material 3 colors. */
-@Composable
-fun MaterialColorsScreen() {
-    val colors = MaterialTheme.colorScheme
-    ColorsScreen(
-        listOf(
-            "primary" to colors.primary,
-            "onPrimary" to colors.onPrimary,
-            "primaryContainer" to colors.primaryContainer,
-            "onPrimaryContainer" to colors.onPrimaryContainer,
-            "inversePrimary" to colors.inversePrimary,
-            "secondary" to colors.secondary,
-            "onSecondary" to colors.onSecondary,
-            "secondaryContainer" to colors.secondaryContainer,
-            "onSecondaryContainer" to colors.onSecondaryContainer,
-            "tertiary" to colors.tertiary,
-            "onTertiary" to colors.onTertiary,
-            "tertiaryContainer" to colors.tertiaryContainer,
-            "onTertiaryContainer" to colors.onTertiaryContainer,
-            "background" to colors.background,
-            "onBackground" to colors.onBackground,
-            "surface" to colors.surface,
-            "onSurface" to colors.onSurface,
-            "surfaceVariant" to colors.surfaceVariant,
-            "onSurfaceVariant" to colors.onSurfaceVariant,
-            "inverseSurface" to colors.inverseSurface,
-            "inverseOnSurface" to colors.inverseOnSurface,
-            "error" to colors.error,
-            "onError" to colors.onError,
-            "errorContainer" to colors.errorContainer,
-            "onErrorContainer" to colors.onErrorContainer,
-            "outline" to colors.outline,
-        )
-    )
-}
-
-/** The screen that shows all the Android colors. */
-@Composable
-fun AndroidColorsScreen() {
-    val colors = LocalAndroidColorScheme.current
-    ColorsScreen(
-        listOf(
-            "colorPrimary" to colors.colorPrimary,
-            "colorPrimaryDark" to colors.colorPrimaryDark,
-            "colorAccent" to colors.colorAccent,
-            "colorAccentPrimary" to colors.colorAccentPrimary,
-            "colorAccentSecondary" to colors.colorAccentSecondary,
-            "colorAccentTertiary" to colors.colorAccentTertiary,
-            "colorAccentPrimaryVariant" to colors.colorAccentPrimaryVariant,
-            "colorAccentSecondaryVariant" to colors.colorAccentSecondaryVariant,
-            "colorAccentTertiaryVariant" to colors.colorAccentTertiaryVariant,
-            "colorSurface" to colors.colorSurface,
-            "colorSurfaceHighlight" to colors.colorSurfaceHighlight,
-            "colorSurfaceVariant" to colors.colorSurfaceVariant,
-            "colorSurfaceHeader" to colors.colorSurfaceHeader,
-            "colorError" to colors.colorError,
-            "colorBackground" to colors.colorBackground,
-            "colorBackgroundFloating" to colors.colorBackgroundFloating,
-            "panelColorBackground" to colors.panelColorBackground,
-            "textColorPrimary" to colors.textColorPrimary,
-            "textColorSecondary" to colors.textColorSecondary,
-            "textColorTertiary" to colors.textColorTertiary,
-            "textColorPrimaryInverse" to colors.textColorPrimaryInverse,
-            "textColorSecondaryInverse" to colors.textColorSecondaryInverse,
-            "textColorTertiaryInverse" to colors.textColorTertiaryInverse,
-            "textColorOnAccent" to colors.textColorOnAccent,
-            "colorForeground" to colors.colorForeground,
-            "colorForegroundInverse" to colors.colorForegroundInverse,
-        )
-    )
-}
-
-@Composable
-private fun ColorsScreen(
-    colors: List<Pair<String, Color>>,
-) {
-    LazyColumn(
-        Modifier.fillMaxWidth(),
-    ) {
-        colors.forEach { (name, color) -> item { ColorTile(color, name) } }
-    }
-}
-
-@Composable
-private fun ColorTile(
-    color: Color,
-    name: String,
-) {
-    Row(
-        Modifier.padding(16.dp),
-        verticalAlignment = Alignment.CenterVertically,
-    ) {
-        val shape = RoundedCornerShape(16.dp)
-        Spacer(
-            Modifier.border(1.dp, MaterialTheme.colorScheme.onBackground, shape)
-                .background(color, shape)
-                .size(64.dp)
-        )
-        Spacer(Modifier.width(16.dp))
-        Text(name)
-    }
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ConfigurationControls.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ConfigurationControls.kt
deleted file mode 100644
index 990d060..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ConfigurationControls.kt
+++ /dev/null
@@ -1,210 +0,0 @@
-package com.android.systemui.compose.gallery
-
-import android.graphics.Point
-import android.os.UserHandle
-import android.view.Display
-import android.view.WindowManagerGlobal
-import androidx.compose.foundation.layout.RowScope
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.width
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.DarkMode
-import androidx.compose.material.icons.filled.FormatSize
-import androidx.compose.material.icons.filled.FormatTextdirectionLToR
-import androidx.compose.material.icons.filled.FormatTextdirectionRToL
-import androidx.compose.material.icons.filled.InvertColors
-import androidx.compose.material.icons.filled.LightMode
-import androidx.compose.material.icons.filled.Smartphone
-import androidx.compose.material.icons.filled.Tablet
-import androidx.compose.material3.Button
-import androidx.compose.material3.ButtonDefaults
-import androidx.compose.material3.Icon
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import kotlin.math.max
-import kotlin.math.min
-
-enum class FontScale(val scale: Float) {
-    Small(0.85f),
-    Normal(1f),
-    Big(1.15f),
-    Bigger(1.30f),
-}
-
-/** A configuration panel that allows to toggle the theme, font scale and layout direction. */
-@Composable
-fun ConfigurationControls(
-    theme: Theme,
-    fontScale: FontScale,
-    layoutDirection: LayoutDirection,
-    onChangeTheme: () -> Unit,
-    onChangeLayoutDirection: () -> Unit,
-    onChangeFontScale: () -> Unit,
-    modifier: Modifier = Modifier,
-) {
-    // The display we are emulating, if any.
-    var emulatedDisplayName by rememberSaveable { mutableStateOf<String?>(null) }
-    val emulatedDisplay =
-        emulatedDisplayName?.let { name -> EmulatedDisplays.firstOrNull { it.name == name } }
-
-    LaunchedEffect(emulatedDisplay) {
-        val wm = WindowManagerGlobal.getWindowManagerService()
-
-        val defaultDisplayId = Display.DEFAULT_DISPLAY
-        if (emulatedDisplay == null) {
-            wm.clearForcedDisplayDensityForUser(defaultDisplayId, UserHandle.myUserId())
-            wm.clearForcedDisplaySize(defaultDisplayId)
-        } else {
-            val density = emulatedDisplay.densityDpi
-
-            // Emulate the display and make sure that we use the maximum available space possible.
-            val initialSize = Point()
-            wm.getInitialDisplaySize(defaultDisplayId, initialSize)
-            val width = emulatedDisplay.width
-            val height = emulatedDisplay.height
-            val minOfSize = min(width, height)
-            val maxOfSize = max(width, height)
-            if (initialSize.x < initialSize.y) {
-                wm.setForcedDisplaySize(defaultDisplayId, minOfSize, maxOfSize)
-            } else {
-                wm.setForcedDisplaySize(defaultDisplayId, maxOfSize, minOfSize)
-            }
-            wm.setForcedDisplayDensityForUser(defaultDisplayId, density, UserHandle.myUserId())
-        }
-    }
-
-    // TODO(b/231131244): Fork FlowRow from Accompanist and use that instead to make sure that users
-    // don't miss any available configuration.
-    LazyRow(modifier) {
-        // Dark/light theme.
-        item {
-            TextButton(onChangeTheme) {
-                val text: String
-                val icon: ImageVector
-
-                when (theme) {
-                    Theme.System -> {
-                        icon = Icons.Default.InvertColors
-                        text = "System"
-                    }
-                    Theme.Dark -> {
-                        icon = Icons.Default.DarkMode
-                        text = "Dark"
-                    }
-                    Theme.Light -> {
-                        icon = Icons.Default.LightMode
-                        text = "Light"
-                    }
-                }
-
-                Icon(icon, null)
-                Spacer(Modifier.width(8.dp))
-                Text(text)
-            }
-        }
-
-        // Font scale.
-        item {
-            TextButton(onChangeFontScale) {
-                Icon(Icons.Default.FormatSize, null)
-                Spacer(Modifier.width(8.dp))
-
-                Text(fontScale.name)
-            }
-        }
-
-        // Layout direction.
-        item {
-            TextButton(onChangeLayoutDirection) {
-                when (layoutDirection) {
-                    LayoutDirection.Ltr -> {
-                        Icon(Icons.Default.FormatTextdirectionLToR, null)
-                        Spacer(Modifier.width(8.dp))
-                        Text("LTR")
-                    }
-                    LayoutDirection.Rtl -> {
-                        Icon(Icons.Default.FormatTextdirectionRToL, null)
-                        Spacer(Modifier.width(8.dp))
-                        Text("RTL")
-                    }
-                }
-            }
-        }
-
-        // Display emulation.
-        EmulatedDisplays.forEach { display ->
-            item {
-                DisplayButton(
-                    display,
-                    emulatedDisplay == display,
-                    { emulatedDisplayName = it?.name },
-                )
-            }
-        }
-    }
-}
-
-@Composable
-private fun DisplayButton(
-    display: EmulatedDisplay,
-    selected: Boolean,
-    onChangeEmulatedDisplay: (EmulatedDisplay?) -> Unit,
-) {
-    val onClick = {
-        if (selected) {
-            onChangeEmulatedDisplay(null)
-        } else {
-            onChangeEmulatedDisplay(display)
-        }
-    }
-
-    val content: @Composable RowScope.() -> Unit = {
-        Icon(display.icon, null)
-        Spacer(Modifier.width(8.dp))
-        Text(display.name)
-    }
-
-    if (selected) {
-        Button(onClick, contentPadding = ButtonDefaults.TextButtonContentPadding, content = content)
-    } else {
-        TextButton(onClick, content = content)
-    }
-}
-
-/** The displays that can be emulated from this Gallery app. */
-private val EmulatedDisplays =
-    listOf(
-        EmulatedDisplay(
-            "Phone",
-            Icons.Default.Smartphone,
-            width = 1440,
-            height = 3120,
-            densityDpi = 560,
-        ),
-        EmulatedDisplay(
-            "Tablet",
-            Icons.Default.Tablet,
-            width = 2560,
-            height = 1600,
-            densityDpi = 320,
-        ),
-    )
-
-private data class EmulatedDisplay(
-    val name: String,
-    val icon: ImageVector,
-    val width: Int,
-    val height: Int,
-    val densityDpi: Int,
-)
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryActivity.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryActivity.kt
deleted file mode 100644
index bb2d2fe..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryActivity.kt
+++ /dev/null
@@ -1,80 +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.compose.gallery
-
-import android.app.UiModeManager
-import android.content.Context
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.graphics.Color
-import androidx.core.view.WindowCompat
-import com.android.systemui.compose.rememberSystemUiController
-
-class GalleryActivity : ComponentActivity() {
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        WindowCompat.setDecorFitsSystemWindows(window, false)
-        val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
-
-        setContent {
-            var theme by rememberSaveable { mutableStateOf(Theme.System) }
-            val onChangeTheme = {
-                // Change to the next theme for a toggle behavior.
-                theme =
-                    when (theme) {
-                        Theme.System -> Theme.Dark
-                        Theme.Dark -> Theme.Light
-                        Theme.Light -> Theme.System
-                    }
-            }
-
-            val isSystemInDarkTheme = isSystemInDarkTheme()
-            val isDark = theme == Theme.Dark || (theme == Theme.System && isSystemInDarkTheme)
-            val useDarkIcons = !isDark
-            val systemUiController = rememberSystemUiController()
-            SideEffect {
-                systemUiController.setSystemBarsColor(
-                    color = Color.Transparent,
-                    darkIcons = useDarkIcons,
-                )
-
-                uiModeManager.setApplicationNightMode(
-                    when (theme) {
-                        Theme.System -> UiModeManager.MODE_NIGHT_AUTO
-                        Theme.Dark -> UiModeManager.MODE_NIGHT_YES
-                        Theme.Light -> UiModeManager.MODE_NIGHT_NO
-                    }
-                )
-            }
-
-            GalleryApp(theme, onChangeTheme)
-        }
-    }
-}
-
-enum class Theme {
-    System,
-    Dark,
-    Light,
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
deleted file mode 100644
index 6805bf8..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/GalleryApp.kt
+++ /dev/null
@@ -1,202 +0,0 @@
-package com.android.systemui.compose.gallery
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBarsPadding
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalLayoutDirection
-import androidx.compose.ui.unit.Density
-import androidx.compose.ui.unit.LayoutDirection
-import androidx.compose.ui.unit.dp
-import androidx.navigation.compose.NavHost
-import androidx.navigation.compose.rememberNavController
-import com.android.systemui.compose.theme.SystemUITheme
-
-/** The gallery app screens. */
-object GalleryAppScreens {
-    private val Typography = ChildScreen("typography") { TypographyScreen() }
-    private val MaterialColors = ChildScreen("material_colors") { MaterialColorsScreen() }
-    private val AndroidColors = ChildScreen("android_colors") { AndroidColorsScreen() }
-    private val Buttons = ChildScreen("buttons") { ButtonsScreen() }
-    private val ExampleFeature = ChildScreen("example_feature") { ExampleFeatureScreen() }
-
-    private val PeopleEmpty =
-        ChildScreen("people_empty") { navController ->
-            EmptyPeopleScreen(onResult = { navController.popBackStack() })
-        }
-    private val PeopleFew =
-        ChildScreen("people_few") { navController ->
-            FewPeopleScreen(onResult = { navController.popBackStack() })
-        }
-    private val PeopleFull =
-        ChildScreen("people_full") { navController ->
-            FullPeopleScreen(onResult = { navController.popBackStack() })
-        }
-    private val People =
-        ParentScreen(
-            "people",
-            mapOf(
-                "Empty" to PeopleEmpty,
-                "Few" to PeopleFew,
-                "Full" to PeopleFull,
-            )
-        )
-    private val UserSwitcherSingleUser =
-        ChildScreen("user_switcher_single") { navController ->
-            UserSwitcherScreen(
-                userCount = 1,
-                onFinished = navController::popBackStack,
-            )
-        }
-    private val UserSwitcherThreeUsers =
-        ChildScreen("user_switcher_three") { navController ->
-            UserSwitcherScreen(
-                userCount = 3,
-                onFinished = navController::popBackStack,
-            )
-        }
-    private val UserSwitcherFourUsers =
-        ChildScreen("user_switcher_four") { navController ->
-            UserSwitcherScreen(
-                userCount = 4,
-                onFinished = navController::popBackStack,
-            )
-        }
-    private val UserSwitcherFiveUsers =
-        ChildScreen("user_switcher_five") { navController ->
-            UserSwitcherScreen(
-                userCount = 5,
-                onFinished = navController::popBackStack,
-            )
-        }
-    private val UserSwitcherSixUsers =
-        ChildScreen("user_switcher_six") { navController ->
-            UserSwitcherScreen(
-                userCount = 6,
-                onFinished = navController::popBackStack,
-            )
-        }
-    private val UserSwitcher =
-        ParentScreen(
-            "user_switcher",
-            mapOf(
-                "Single" to UserSwitcherSingleUser,
-                "Three" to UserSwitcherThreeUsers,
-                "Four" to UserSwitcherFourUsers,
-                "Five" to UserSwitcherFiveUsers,
-                "Six" to UserSwitcherSixUsers,
-            )
-        )
-
-    val Home =
-        ParentScreen(
-            "home",
-            mapOf(
-                "Typography" to Typography,
-                "Material colors" to MaterialColors,
-                "Android colors" to AndroidColors,
-                "Example feature" to ExampleFeature,
-                "Buttons" to Buttons,
-                "People" to People,
-                "User Switcher" to UserSwitcher,
-            )
-        )
-}
-
-/** The main content of the app, that shows [GalleryAppScreens.Home] by default. */
-@Composable
-private fun MainContent(onControlToggleRequested: () -> Unit) {
-    Box(Modifier.fillMaxSize()) {
-        val navController = rememberNavController()
-        NavHost(
-            navController = navController,
-            startDestination = GalleryAppScreens.Home.identifier,
-        ) {
-            screen(GalleryAppScreens.Home, navController, onControlToggleRequested)
-        }
-    }
-}
-
-/**
- * The top-level composable shown when starting the app. This composable always shows a
- * [ConfigurationControls] at the top of the screen, above the [MainContent].
- */
-@Composable
-fun GalleryApp(
-    theme: Theme,
-    onChangeTheme: () -> Unit,
-) {
-    val systemFontScale = LocalDensity.current.fontScale
-    var fontScale: FontScale by rememberSaveable {
-        mutableStateOf(
-            FontScale.values().firstOrNull { it.scale == systemFontScale } ?: FontScale.Normal
-        )
-    }
-    val context = LocalContext.current
-    val density = Density(context.resources.displayMetrics.density, fontScale.scale)
-    val onChangeFontScale = {
-        fontScale =
-            when (fontScale) {
-                FontScale.Small -> FontScale.Normal
-                FontScale.Normal -> FontScale.Big
-                FontScale.Big -> FontScale.Bigger
-                FontScale.Bigger -> FontScale.Small
-            }
-    }
-
-    val systemLayoutDirection = LocalLayoutDirection.current
-    var layoutDirection by rememberSaveable { mutableStateOf(systemLayoutDirection) }
-    val onChangeLayoutDirection = {
-        layoutDirection =
-            when (layoutDirection) {
-                LayoutDirection.Ltr -> LayoutDirection.Rtl
-                LayoutDirection.Rtl -> LayoutDirection.Ltr
-            }
-    }
-
-    CompositionLocalProvider(
-        LocalDensity provides density,
-        LocalLayoutDirection provides layoutDirection,
-    ) {
-        SystemUITheme {
-            Surface(
-                Modifier.fillMaxSize(),
-                color = MaterialTheme.colorScheme.background,
-            ) {
-                Column(Modifier.fillMaxSize().systemBarsPadding()) {
-                    var showControls by rememberSaveable { mutableStateOf(true) }
-
-                    if (showControls) {
-                        ConfigurationControls(
-                            theme,
-                            fontScale,
-                            layoutDirection,
-                            onChangeTheme,
-                            onChangeLayoutDirection,
-                            onChangeFontScale,
-                            Modifier.padding(horizontal = 16.dp),
-                        )
-
-                        Spacer(Modifier.height(4.dp))
-                    }
-
-                    MainContent(onControlToggleRequested = { showControls = !showControls })
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt
deleted file mode 100644
index 2f0df77..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/PeopleScreen.kt
+++ /dev/null
@@ -1,46 +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.compose.gallery
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalContext
-import com.android.systemui.people.emptyPeopleSpaceViewModel
-import com.android.systemui.people.fewPeopleSpaceViewModel
-import com.android.systemui.people.fullPeopleSpaceViewModel
-import com.android.systemui.people.ui.compose.PeopleScreen
-import com.android.systemui.people.ui.viewmodel.PeopleViewModel
-
-@Composable
-fun EmptyPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
-    val context = LocalContext.current.applicationContext
-    val viewModel = emptyPeopleSpaceViewModel(context)
-    PeopleScreen(viewModel, onResult)
-}
-
-@Composable
-fun FewPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
-    val context = LocalContext.current.applicationContext
-    val viewModel = fewPeopleSpaceViewModel(context)
-    PeopleScreen(viewModel, onResult)
-}
-
-@Composable
-fun FullPeopleScreen(onResult: (PeopleViewModel.Result) -> Unit) {
-    val context = LocalContext.current.applicationContext
-    val viewModel = fullPeopleSpaceViewModel(context)
-    PeopleScreen(viewModel, onResult)
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
deleted file mode 100644
index d7d0d72..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/Screen.kt
+++ /dev/null
@@ -1,126 +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.compose.gallery
-
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavController
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.navigation
-
-/**
- * A screen in an app. It is either an [ParentScreen] which lists its child screens to navigate to
- * them or a [ChildScreen] which shows some content.
- */
-sealed class Screen(val identifier: String)
-
-class ParentScreen(
-    identifier: String,
-    val children: Map<String, Screen>,
-) : Screen(identifier)
-
-class ChildScreen(
-    identifier: String,
-    val content: @Composable (NavController) -> Unit,
-) : Screen(identifier)
-
-/** Create the navigation graph for [screen]. */
-fun NavGraphBuilder.screen(
-    screen: Screen,
-    navController: NavController,
-    onControlToggleRequested: () -> Unit,
-) {
-    when (screen) {
-        is ChildScreen -> composable(screen.identifier) { screen.content(navController) }
-        is ParentScreen -> {
-            val menuRoute = "${screen.identifier}_menu"
-            navigation(startDestination = menuRoute, route = screen.identifier) {
-                // The menu to navigate to one of the children screens.
-                composable(menuRoute) {
-                    ScreenMenu(screen, navController, onControlToggleRequested)
-                }
-
-                // The content of the child screens.
-                screen.children.forEach { (_, child) ->
-                    screen(
-                        child,
-                        navController,
-                        onControlToggleRequested,
-                    )
-                }
-            }
-        }
-    }
-}
-
-@Composable
-private fun ScreenMenu(
-    screen: ParentScreen,
-    navController: NavController,
-    onControlToggleRequested: () -> Unit,
-) {
-    LazyColumn(
-        Modifier.padding(horizontal = 16.dp),
-        verticalArrangement = Arrangement.spacedBy(8.dp),
-    ) {
-        item {
-            Surface(
-                Modifier.fillMaxWidth(),
-                color = MaterialTheme.colorScheme.tertiaryContainer,
-                shape = CircleShape,
-            ) {
-                Column(
-                    Modifier.clickable(onClick = onControlToggleRequested).padding(16.dp),
-                    horizontalAlignment = Alignment.CenterHorizontally,
-                ) {
-                    Text("Toggle controls")
-                }
-            }
-        }
-
-        screen.children.forEach { (name, child) ->
-            item {
-                Surface(
-                    Modifier.fillMaxWidth(),
-                    color = MaterialTheme.colorScheme.secondaryContainer,
-                    shape = CircleShape,
-                ) {
-                    Column(
-                        Modifier.clickable { navController.navigate(child.identifier) }
-                            .padding(16.dp),
-                        horizontalAlignment = Alignment.CenterHorizontally,
-                    ) {
-                        Text(name)
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/TypographyScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/TypographyScreen.kt
deleted file mode 100644
index 147025e..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/TypographyScreen.kt
+++ /dev/null
@@ -1,67 +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.compose.gallery
-
-import androidx.compose.foundation.horizontalScroll
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.text.TextStyle
-import androidx.compose.ui.text.style.TextOverflow
-
-/** The screen that shows the Material text styles. */
-@Composable
-fun TypographyScreen() {
-    val typography = MaterialTheme.typography
-
-    Column(
-        Modifier.fillMaxSize()
-            .horizontalScroll(rememberScrollState())
-            .verticalScroll(rememberScrollState()),
-    ) {
-        FontLine("displayLarge", typography.displayLarge)
-        FontLine("displayMedium", typography.displayMedium)
-        FontLine("displaySmall", typography.displaySmall)
-        FontLine("headlineLarge", typography.headlineLarge)
-        FontLine("headlineMedium", typography.headlineMedium)
-        FontLine("headlineSmall", typography.headlineSmall)
-        FontLine("titleLarge", typography.titleLarge)
-        FontLine("titleMedium", typography.titleMedium)
-        FontLine("titleSmall", typography.titleSmall)
-        FontLine("bodyLarge", typography.bodyLarge)
-        FontLine("bodyMedium", typography.bodyMedium)
-        FontLine("bodySmall", typography.bodySmall)
-        FontLine("labelLarge", typography.labelLarge)
-        FontLine("labelMedium", typography.labelMedium)
-        FontLine("labelSmall", typography.labelSmall)
-    }
-}
-
-@Composable
-private fun FontLine(name: String, style: TextStyle) {
-    Text(
-        "$name (${style.fontSize}/${style.lineHeight}, W${style.fontWeight?.weight})",
-        style = style,
-        maxLines = 1,
-        overflow = TextOverflow.Visible,
-    )
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/UserSwitcherScreen.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/UserSwitcherScreen.kt
deleted file mode 100644
index fe9707d..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/UserSwitcherScreen.kt
+++ /dev/null
@@ -1,35 +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.compose.gallery
-
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.platform.LocalContext
-import com.android.systemui.user.Fakes.fakeUserSwitcherViewModel
-import com.android.systemui.user.ui.compose.UserSwitcherScreen
-
-@Composable
-fun UserSwitcherScreen(
-    userCount: Int,
-    onFinished: () -> Unit,
-) {
-    val context = LocalContext.current.applicationContext
-    UserSwitcherScreen(
-        viewModel = fakeUserSwitcherViewModel(context, userCount = userCount),
-        onFinished = onFinished,
-    )
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt
deleted file mode 100644
index 0966c32..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/people/Fakes.kt
+++ /dev/null
@@ -1,156 +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.people
-
-import android.content.Context
-import android.graphics.Bitmap
-import android.graphics.Canvas
-import android.graphics.Color
-import android.graphics.Paint
-import android.graphics.drawable.Icon
-import androidx.core.graphics.drawable.toIcon
-import com.android.systemui.R
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.people.data.model.PeopleTileModel
-import com.android.systemui.people.ui.viewmodel.PeopleViewModel
-import com.android.systemui.people.widget.PeopleTileKey
-
-/** A [PeopleViewModel] that does not have any conversations. */
-fun emptyPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
-    return fakePeopleSpaceViewModel(context, emptyList(), emptyList())
-}
-
-/** A [PeopleViewModel] that has a few conversations. */
-fun fewPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
-    return fakePeopleSpaceViewModel(
-        context,
-        priorityTiles =
-            listOf(
-                fakeTile(context, id = "0", Color.RED, "Priority"),
-                fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
-            ),
-        recentTiles =
-            listOf(
-                fakeTile(context, id = "2", Color.GREEN, "Recent Important", isImportant = true),
-                fakeTile(context, id = "3", Color.CYAN, "Recent DndBlocking", isDndBlocking = true),
-            ),
-    )
-}
-
-/** A [PeopleViewModel] that has a lot of conversations. */
-fun fullPeopleSpaceViewModel(@Application context: Context): PeopleViewModel {
-    return fakePeopleSpaceViewModel(
-        context,
-        priorityTiles =
-            listOf(
-                fakeTile(context, id = "0", Color.RED, "Priority"),
-                fakeTile(context, id = "1", Color.BLUE, "Priority NewStory", hasNewStory = true),
-                fakeTile(context, id = "2", Color.GREEN, "Priority Important", isImportant = true),
-                fakeTile(
-                    context,
-                    id = "3",
-                    Color.CYAN,
-                    "Priority DndBlocking",
-                    isDndBlocking = true,
-                ),
-                fakeTile(
-                    context,
-                    id = "4",
-                    Color.MAGENTA,
-                    "Priority NewStory Important",
-                    hasNewStory = true,
-                    isImportant = true,
-                ),
-            ),
-        recentTiles =
-            listOf(
-                fakeTile(
-                    context,
-                    id = "5",
-                    Color.RED,
-                    "Recent NewStory DndBlocking",
-                    hasNewStory = true,
-                    isDndBlocking = true,
-                ),
-                fakeTile(
-                    context,
-                    id = "6",
-                    Color.BLUE,
-                    "Recent Important DndBlocking",
-                    isImportant = true,
-                    isDndBlocking = true,
-                ),
-                fakeTile(
-                    context,
-                    id = "7",
-                    Color.GREEN,
-                    "Recent NewStory Important DndBlocking",
-                    hasNewStory = true,
-                    isImportant = true,
-                    isDndBlocking = true,
-                ),
-                fakeTile(context, id = "8", Color.CYAN, "Recent"),
-                fakeTile(context, id = "9", Color.MAGENTA, "Recent"),
-            ),
-    )
-}
-
-private fun fakePeopleSpaceViewModel(
-    @Application context: Context,
-    priorityTiles: List<PeopleTileModel>,
-    recentTiles: List<PeopleTileModel>,
-): PeopleViewModel {
-    return PeopleViewModel(
-        context,
-        FakePeopleTileRepository(priorityTiles, recentTiles),
-        FakePeopleWidgetRepository(),
-    )
-}
-
-private fun fakeTile(
-    @Application context: Context,
-    id: String,
-    iconColor: Int,
-    username: String,
-    hasNewStory: Boolean = false,
-    isImportant: Boolean = false,
-    isDndBlocking: Boolean = false
-): PeopleTileModel {
-    return PeopleTileModel(
-        PeopleTileKey(id, /* userId= */ 0, /* packageName */ ""),
-        username,
-        fakeUserIcon(context, iconColor),
-        hasNewStory,
-        isImportant,
-        isDndBlocking,
-    )
-}
-
-private fun fakeUserIcon(@Application context: Context, color: Int): Icon {
-    val size = context.resources.getDimensionPixelSize(R.dimen.avatar_size_for_medium)
-    val bitmap =
-        Bitmap.createBitmap(
-            size,
-            size,
-            Bitmap.Config.ARGB_8888,
-        )
-    val canvas = Canvas(bitmap)
-    val paint = Paint().apply { this.color = color }
-    val radius = size / 2f
-    canvas.drawCircle(/* cx= */ radius, /* cy= */ radius, /* radius= */ radius, paint)
-    return bitmap.toIcon()
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/qs/footer/Fakes.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/qs/footer/Fakes.kt
deleted file mode 100644
index 6588e22..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/qs/footer/Fakes.kt
+++ /dev/null
@@ -1,164 +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.qs.footer
-
-import android.content.Context
-import android.os.UserHandle
-import android.view.View
-import com.android.internal.util.UserIcons
-import com.android.systemui.R
-import com.android.systemui.animation.Expandable
-import com.android.systemui.classifier.FalsingManagerFake
-import com.android.systemui.common.shared.model.Icon
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.globalactions.GlobalActionsDialogLite
-import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel
-import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor
-import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig
-import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
-import com.android.systemui.util.mockito.mock
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-
-/** A list of fake [FooterActionsViewModel] to be used in screenshot tests and the gallery. */
-fun fakeFooterActionsViewModels(
-    @Application context: Context,
-): List<FooterActionsViewModel> {
-    return listOf(
-        fakeFooterActionsViewModel(context),
-        fakeFooterActionsViewModel(context, showPowerButton = false, isGuestUser = true),
-        fakeFooterActionsViewModel(context, showUserSwitcher = false),
-        fakeFooterActionsViewModel(context, showUserSwitcher = false, foregroundServices = 4),
-        fakeFooterActionsViewModel(
-            context,
-            foregroundServices = 4,
-            hasNewForegroundServices = true,
-            userId = 1,
-        ),
-        fakeFooterActionsViewModel(
-            context,
-            securityText = "Security",
-            foregroundServices = 4,
-            showUserSwitcher = false,
-        ),
-        fakeFooterActionsViewModel(
-            context,
-            securityText = "Security (not clickable)",
-            securityClickable = false,
-            foregroundServices = 4,
-            hasNewForegroundServices = true,
-            userId = 2,
-        ),
-    )
-}
-
-private fun fakeFooterActionsViewModel(
-    @Application context: Context,
-    securityText: String? = null,
-    securityClickable: Boolean = true,
-    foregroundServices: Int = 0,
-    hasNewForegroundServices: Boolean = false,
-    showUserSwitcher: Boolean = true,
-    showPowerButton: Boolean = true,
-    userId: Int = UserHandle.USER_OWNER,
-    isGuestUser: Boolean = false,
-): FooterActionsViewModel {
-    val interactor =
-        FakeFooterActionsInteractor(
-            securityButtonConfig =
-                flowOf(
-                    securityText?.let { text ->
-                        SecurityButtonConfig(
-                            icon =
-                                Icon.Resource(
-                                    R.drawable.ic_info_outline,
-                                    contentDescription = null,
-                                ),
-                            text = text,
-                            isClickable = securityClickable,
-                        )
-                    }
-                ),
-            foregroundServicesCount = flowOf(foregroundServices),
-            hasNewForegroundServices = flowOf(hasNewForegroundServices),
-            userSwitcherStatus =
-                flowOf(
-                    if (showUserSwitcher) {
-                        UserSwitcherStatusModel.Enabled(
-                            currentUserName = "foo",
-                            currentUserImage =
-                                UserIcons.getDefaultUserIcon(
-                                    context.resources,
-                                    userId,
-                                    /* light= */ false,
-                                ),
-                            isGuestUser = isGuestUser,
-                        )
-                    } else {
-                        UserSwitcherStatusModel.Disabled
-                    }
-                ),
-            deviceMonitoringDialogRequests = flowOf(),
-        )
-
-    return FooterActionsViewModel(
-        context,
-        interactor,
-        FalsingManagerFake(),
-        globalActionsDialogLite = mock(),
-        showPowerButton = showPowerButton,
-    )
-}
-
-private class FakeFooterActionsInteractor(
-    override val securityButtonConfig: Flow<SecurityButtonConfig?> = flowOf(null),
-    override val foregroundServicesCount: Flow<Int> = flowOf(0),
-    override val hasNewForegroundServices: Flow<Boolean> = flowOf(false),
-    override val userSwitcherStatus: Flow<UserSwitcherStatusModel> =
-        flowOf(UserSwitcherStatusModel.Disabled),
-    override val deviceMonitoringDialogRequests: Flow<Unit> = flowOf(),
-    private val onShowDeviceMonitoringDialogFromView: (View) -> Unit = {},
-    private val onShowDeviceMonitoringDialog: (Context) -> Unit = {},
-    private val onShowForegroundServicesDialog: (View) -> Unit = {},
-    private val onShowPowerMenuDialog: (GlobalActionsDialogLite, View) -> Unit = { _, _ -> },
-    private val onShowSettings: (Expandable) -> Unit = {},
-    private val onShowUserSwitcher: (View) -> Unit = {},
-) : FooterActionsInteractor {
-    override fun showDeviceMonitoringDialog(view: View) {
-        onShowDeviceMonitoringDialogFromView(view)
-    }
-
-    override fun showDeviceMonitoringDialog(quickSettingsContext: Context) {
-        onShowDeviceMonitoringDialog(quickSettingsContext)
-    }
-
-    override fun showForegroundServicesDialog(view: View) {
-        onShowForegroundServicesDialog(view)
-    }
-
-    override fun showPowerMenuDialog(globalActionsDialogLite: GlobalActionsDialogLite, view: View) {
-        onShowPowerMenuDialog(globalActionsDialogLite, view)
-    }
-
-    override fun showSettings(expandable: Expandable) {
-        onShowSettings(expandable)
-    }
-
-    override fun showUserSwitcher(view: View) {
-        onShowUserSwitcher(view)
-    }
-}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/user/Fakes.kt b/packages/SystemUI/compose/gallery/src/com/android/systemui/user/Fakes.kt
deleted file mode 100644
index 91a73ea..0000000
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/user/Fakes.kt
+++ /dev/null
@@ -1,116 +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.user
-
-import android.content.Context
-import androidx.appcompat.content.res.AppCompatResources
-import com.android.systemui.common.shared.model.Text
-import com.android.systemui.compose.gallery.R
-import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
-import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
-import com.android.systemui.power.data.repository.FakePowerRepository
-import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.domain.interactor.UserInteractor
-import com.android.systemui.user.shared.model.UserActionModel
-import com.android.systemui.user.shared.model.UserModel
-import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
-import com.android.systemui.util.mockito.mock
-
-object Fakes {
-    private val USER_TINT_COLORS =
-        arrayOf(
-            0x000000,
-            0x0000ff,
-            0x00ff00,
-            0x00ffff,
-            0xff0000,
-            0xff00ff,
-            0xffff00,
-            0xffffff,
-        )
-
-    fun fakeUserSwitcherViewModel(
-        context: Context,
-        userCount: Int,
-    ): UserSwitcherViewModel {
-        return UserSwitcherViewModel.Factory(
-                userInteractor =
-                    UserInteractor(
-                        repository =
-                            FakeUserRepository().apply {
-                                setUsers(
-                                    (0 until userCount).map { index ->
-                                        UserModel(
-                                            id = index,
-                                            name =
-                                                Text.Loaded(
-                                                    when (index % 6) {
-                                                        0 -> "Ross Geller"
-                                                        1 -> "Phoebe Buffay"
-                                                        2 -> "Monica Geller"
-                                                        3 -> "Rachel Greene"
-                                                        4 -> "Chandler Bing"
-                                                        else -> "Joey Tribbiani"
-                                                    }
-                                                ),
-                                            image =
-                                                checkNotNull(
-                                                    AppCompatResources.getDrawable(
-                                                        context,
-                                                        when (index % 6) {
-                                                            0 -> R.drawable.kitten1
-                                                            1 -> R.drawable.kitten2
-                                                            2 -> R.drawable.kitten3
-                                                            3 -> R.drawable.kitten4
-                                                            4 -> R.drawable.kitten5
-                                                            else -> R.drawable.kitten6
-                                                        },
-                                                    )
-                                                ),
-                                            isSelected = index == 0,
-                                            isSelectable = true,
-                                        )
-                                    }
-                                )
-                                setActions(
-                                    UserActionModel.values().mapNotNull {
-                                        if (it == UserActionModel.NAVIGATE_TO_USER_MANAGEMENT) {
-                                            null
-                                        } else {
-                                            it
-                                        }
-                                    }
-                                )
-                            },
-                        controller = mock(),
-                        activityStarter = mock(),
-                        keyguardInteractor =
-                            KeyguardInteractor(
-                                repository =
-                                    FakeKeyguardRepository().apply { setKeyguardShowing(false) },
-                            ),
-                    ),
-                powerInteractor =
-                    PowerInteractor(
-                        repository = FakePowerRepository(),
-                    )
-            )
-            .create(UserSwitcherViewModel::class.java)
-    }
-}
diff --git a/packages/SystemUI/compose/gallery/tests/Android.bp b/packages/SystemUI/compose/gallery/tests/Android.bp
deleted file mode 100644
index 3e01f7d..0000000
--- a/packages/SystemUI/compose/gallery/tests/Android.bp
+++ /dev/null
@@ -1,47 +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 {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "frameworks_base_packages_SystemUI_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["frameworks_base_packages_SystemUI_license"],
-}
-
-android_test {
-    name: "SystemUIComposeGalleryTests",
-    manifest: "AndroidManifest.xml",
-    test_suites: ["device-tests"],
-    sdk_version: "current",
-    certificate: "platform",
-
-    srcs: [
-        "src/**/*.kt",
-    ],
-
-    static_libs: [
-        "SystemUIComposeGalleryLib",
-
-        "androidx.test.runner",
-        "androidx.test.ext.junit",
-
-        "androidx.compose.runtime_runtime",
-        "androidx.compose.ui_ui-test-junit4",
-        "androidx.compose.ui_ui-test-manifest",
-    ],
-
-    kotlincflags: ["-Xjvm-default=enable"],
-}
diff --git a/packages/SystemUI/compose/gallery/tests/AndroidManifest.xml b/packages/SystemUI/compose/gallery/tests/AndroidManifest.xml
deleted file mode 100644
index 5eeb3ad..0000000
--- a/packages/SystemUI/compose/gallery/tests/AndroidManifest.xml
+++ /dev/null
@@ -1,28 +0,0 @@
-<?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.compose.gallery.tests" >
-
-    <application>
-        <uses-library android:name="android.test.runner" />
-    </application>
-
-    <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
-                     android:targetPackage="com.android.systemui.compose.gallery.tests"
-                     android:label="Tests for SystemUIComposeGallery"/>
-
-</manifest>
\ No newline at end of file
diff --git a/packages/SystemUI/compose/gallery/tests/src/com/android/systemui/compose/gallery/ScreenshotsTests.kt b/packages/SystemUI/compose/gallery/tests/src/com/android/systemui/compose/gallery/ScreenshotsTests.kt
deleted file mode 100644
index 66ecc8d..0000000
--- a/packages/SystemUI/compose/gallery/tests/src/com/android/systemui/compose/gallery/ScreenshotsTests.kt
+++ /dev/null
@@ -1,36 +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.compose.gallery
-
-import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import com.android.systemui.compose.theme.SystemUITheme
-import org.junit.Rule
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class ScreenshotsTests {
-    @get:Rule val composeRule = createComposeRule()
-
-    @Test
-    fun exampleFeatureScreenshotTest() {
-        // TODO(b/230832101): Wire this with the screenshot diff testing infra. We should reuse the
-        // configuration of the features in the gallery app to populate the UIs.
-        composeRule.setContent { SystemUITheme { ExampleFeatureScreen() } }
-    }
-}
diff --git a/packages/SystemUI/ktfmt_includes.txt b/packages/SystemUI/ktfmt_includes.txt
index 8182484..9f275af 100644
--- a/packages/SystemUI/ktfmt_includes.txt
+++ b/packages/SystemUI/ktfmt_includes.txt
@@ -813,7 +813,7 @@
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallLoggerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/panelstate/PanelExpansionStateManagerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerTest.kt
+-packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLoggerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/WifiRepositoryImplTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorTest.kt
@@ -828,7 +828,7 @@
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherAdapterTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/RemoteInputQuickSettingsDisablerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SafetyControllerTest.kt
--packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
+-packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/VariableDateViewControllerTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/WalletControllerImplTest.kt
 -packages/SystemUI/tests/src/com/android/systemui/statusbar/window/StatusBarWindowStateControllerTest.kt
diff --git a/packages/SystemUI/res/layout/media_projection_app_selector.xml b/packages/SystemUI/res/layout/media_projection_app_selector.xml
index 4ad6849..226bc6a 100644
--- a/packages/SystemUI/res/layout/media_projection_app_selector.xml
+++ b/packages/SystemUI/res/layout/media_projection_app_selector.xml
@@ -36,13 +36,14 @@
         android:background="@*android:drawable/bottomsheet_background">
 
         <ImageView
-            android:id="@*android:id/icon"
             android:layout_width="@dimen/media_projection_app_selector_icon_size"
             android:layout_height="@dimen/media_projection_app_selector_icon_size"
             android:layout_marginTop="@*android:dimen/chooser_edge_margin_normal"
             android:layout_marginBottom="@*android:dimen/chooser_edge_margin_normal"
             android:importantForAccessibility="no"
-            android:tint="?android:attr/textColorPrimary"/>
+            android:tint="?android:attr/textColorPrimary"
+            android:src="@drawable/ic_present_to_all"
+            />
 
         <TextView android:id="@*android:id/title"
             android:layout_height="wrap_content"
diff --git a/packages/SystemUI/res/layout/media_projection_recent_tasks.xml b/packages/SystemUI/res/layout/media_projection_recent_tasks.xml
new file mode 100644
index 0000000..a2b3c40
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_projection_recent_tasks.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:orientation="vertical"
+    android:background="?android:attr/colorBackground"
+    >
+
+    <FrameLayout
+        android:layout_width="match_parent"
+        android:layout_height="256dp">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/media_projection_recent_tasks_recycler"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:visibility="gone"
+            />
+
+        <ProgressBar
+            android:id="@+id/media_projection_recent_tasks_loader"
+            android:layout_width="@dimen/media_projection_app_selector_loader_size"
+            android:layout_height="@dimen/media_projection_app_selector_loader_size"
+            android:layout_gravity="center"
+            android:indeterminate="true"
+            android:indeterminateOnly="true" />
+    </FrameLayout>
+
+    <!-- Divider line -->
+    <ImageView
+        android:layout_width="72dp"
+        android:layout_height="2dp"
+        android:layout_marginBottom="8dp"
+        android:layout_marginTop="24dp"
+        android:importantForAccessibility="no"
+        android:src="@*android:drawable/ic_drag_handle"
+        android:tint="?android:attr/textColorSecondary" />
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/media_projection_task_item.xml b/packages/SystemUI/res/layout/media_projection_task_item.xml
new file mode 100644
index 0000000..75f73cd
--- /dev/null
+++ b/packages/SystemUI/res/layout/media_projection_task_item.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center"
+    android:gravity="center"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/task_icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:layout_margin="8dp"
+        android:importantForAccessibility="no" />
+
+    <!-- TODO(b/240924926) use a custom view that will handle thumbnail cropping correctly -->
+    <!-- TODO(b/240924926) dynamically change the view size based on the screen size -->
+    <ImageView
+        android:id="@+id/task_thumbnail"
+        android:layout_width="100dp"
+        android:layout_height="216dp"
+        android:scaleType="centerCrop"
+        />
+</LinearLayout>
diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml
index 0c57b934..8388b67 100644
--- a/packages/SystemUI/res/layout/super_notification_shade.xml
+++ b/packages/SystemUI/res/layout/super_notification_shade.xml
@@ -103,6 +103,7 @@
                      android:layout_width="match_parent"
                      android:layout_weight="1"
                      android:background="@android:color/transparent"
+                     android:visibility="invisible"
                      android:clipChildren="false"
                      android:clipToPadding="false" />
     </LinearLayout>
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index f4d4824..b24ce12 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -66,8 +66,6 @@
     <dimen name="lockscreen_shade_qs_transition_distance">@dimen/lockscreen_shade_notifications_scrim_transition_distance</dimen>
     <dimen name="lockscreen_shade_qs_transition_delay">@dimen/lockscreen_shade_notifications_scrim_transition_delay</dimen>
     <dimen name="lockscreen_shade_qs_squish_transition_distance">@dimen/lockscreen_shade_qs_transition_distance</dimen>
-    <!-- On split-shade, the QS squish transition should start from half height.  -->
-    <item name="lockscreen_shade_qs_squish_start_fraction" type="dimen" format="float" >0.5</item>
     <!-- On split-shade, there should be no depth effect, so setting the value to 0.  -->
     <dimen name="lockscreen_shade_depth_controller_transition_distance">0dp</dimen>
     <dimen name="lockscreen_shade_udfps_keyguard_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index a587e5a..5dcbeb5 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -86,8 +86,6 @@
     <dimen name="lockscreen_shade_qs_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
     <dimen name="lockscreen_shade_qs_transition_delay">@dimen/lockscreen_shade_scrim_transition_distance</dimen>
     <dimen name="lockscreen_shade_qs_squish_transition_distance">@dimen/lockscreen_shade_qs_transition_distance</dimen>
-    <!-- On large screen portrait, the QS squish transition should start from half height.  -->
-    <item name="lockscreen_shade_qs_squish_start_fraction" type="dimen" format="float" >0.5</item>
     <dimen name="lockscreen_shade_depth_controller_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
     <dimen name="lockscreen_shade_udfps_keyguard_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
     <dimen name="lockscreen_shade_status_bar_transition_distance">@dimen/lockscreen_shade_full_transition_distance</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index eb6c457..f7019dc 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -1174,6 +1174,7 @@
 
     <!-- Output switcher panel related dimensions -->
     <dimen name="media_output_dialog_list_max_height">355dp</dimen>
+    <dimen name="media_output_dialog_list_item_height">76dp</dimen>
     <dimen name="media_output_dialog_header_album_icon_size">72dp</dimen>
     <dimen name="media_output_dialog_header_back_icon_size">32dp</dimen>
     <dimen name="media_output_dialog_header_icon_padding">16dp</dimen>
@@ -1221,7 +1222,7 @@
 
     <!-- The fraction at which the QS "squish" transition should start during the lockscreen shade
          expansion. 0 is fully collapsed, 1 is fully expanded. -->
-    <item type="dimen" format="float" name="lockscreen_shade_qs_squish_start_fraction">0</item>
+    <item type="dimen" format="float" name="lockscreen_shade_qs_squish_start_fraction">0.5</item>
 
     <!-- Distance that the full shade transition takes in order for depth of the wallpaper to fully
          change.  -->
@@ -1454,6 +1455,8 @@
     <dimen name="fgs_manager_list_top_spacing">12dp</dimen>
 
     <dimen name="media_projection_app_selector_icon_size">32dp</dimen>
+    <dimen name="media_projection_app_selector_recents_padding">16dp</dimen>
+    <dimen name="media_projection_app_selector_loader_size">32dp</dimen>
 
     <!-- Dream overlay related dimensions -->
     <dimen name="dream_overlay_status_bar_height">60dp</dimen>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
index f0210fd..5d6598d 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InteractionJankMonitorWrapper.java
@@ -49,6 +49,8 @@
             InteractionJankMonitor.CUJ_LAUNCHER_APP_LAUNCH_FROM_WIDGET;
     public static final int CUJ_SPLIT_SCREEN_ENTER =
             InteractionJankMonitor.CUJ_SPLIT_SCREEN_ENTER;
+    public static final int CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION =
+            InteractionJankMonitor.CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION;
 
     @IntDef({
             CUJ_APP_LAUNCH_FROM_RECENTS,
@@ -57,6 +59,7 @@
             CUJ_APP_CLOSE_TO_PIP,
             CUJ_QUICK_SWITCH,
             CUJ_APP_LAUNCH_FROM_WIDGET,
+            CUJ_LAUNCHER_UNLOCK_ENTRANCE_ANIMATION
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface CujType {
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
index f82e7db..71470e8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
@@ -52,17 +52,15 @@
     val becauseCannotSkipBouncer: Boolean,
     val biometricSettingEnabledForUser: Boolean,
     val bouncerFullyShown: Boolean,
-    val bouncerIsOrWillShow: Boolean,
     val faceAuthenticated: Boolean,
     val faceDisabled: Boolean,
     val faceLockedOut: Boolean,
     val fpLockedOut: Boolean,
     val goingToSleep: Boolean,
-    val keyguardAwakeExcludingBouncerShowing: Boolean,
+    val keyguardAwake: Boolean,
     val keyguardGoingAway: Boolean,
     val listeningForFaceAssistant: Boolean,
     val occludingAppRequestingFaceAuth: Boolean,
-    val onlyFaceEnrolled: Boolean,
     val primaryUser: Boolean,
     val scanningAllowedByStrongAuth: Boolean,
     val secureCameraLaunched: Boolean,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index f73c98e..2bdb1b8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -22,8 +22,6 @@
 import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP;
 
 import static com.android.systemui.plugins.FalsingManager.LOW_PENALTY;
-import static com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA;
-import static com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA;
 
 import static java.lang.Integer.max;
 
@@ -87,8 +85,8 @@
 import com.android.systemui.animation.Interpolators;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.shared.system.SysUiStatsLog;
+import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
-import com.android.systemui.statusbar.policy.UserSwitcherController.BaseUserAdapter;
 import com.android.systemui.user.data.source.UserRecord;
 import com.android.systemui.util.settings.GlobalSettings;
 
@@ -1098,6 +1096,7 @@
                 return;
             }
 
+            mView.setAlpha(1f);
             mUserSwitcherViewGroup.setAlpha(0f);
             ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(mUserSwitcherViewGroup, View.ALPHA,
                     1f);
@@ -1137,7 +1136,7 @@
 
             KeyguardUserSwitcherAnchor anchor = mView.findViewById(R.id.user_switcher_anchor);
 
-            BaseUserAdapter adapter = new BaseUserAdapter(mUserSwitcherController) {
+            BaseUserSwitcherAdapter adapter = new BaseUserSwitcherAdapter(mUserSwitcherController) {
                 @Override
                 public View getView(int position, View convertView, ViewGroup parent) {
                     UserRecord item = getItem(position);
@@ -1172,8 +1171,7 @@
                     }
                     textView.setSelected(item == currentUser);
                     view.setEnabled(item.isSwitchToEnabled);
-                    view.setAlpha(view.isEnabled() ? USER_SWITCH_ENABLED_ALPHA :
-                            USER_SWITCH_DISABLED_ALPHA);
+                    UserSwitcherController.setSelectableAlpha(view);
                     return view;
                 }
 
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
index d8cffd7..5995e85 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSimPukViewController.java
@@ -107,6 +107,14 @@
     }
 
     @Override
+    public void onResume(int reason) {
+        super.onResume(reason);
+        if (mShowDefaultMessage) {
+            showDefaultMessage();
+        }
+    }
+
+    @Override
     void resetState() {
         super.resetState();
         mStateMachine.reset();
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 32c1cf9..6eef3b3 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -2593,11 +2593,8 @@
         }
 
         final boolean statusBarShadeLocked = mStatusBarState == StatusBarState.SHADE_LOCKED;
-        // mKeyguardIsVisible is true even when the bouncer is shown, we don't want to run face auth
-        // on bouncer if both fp and fingerprint are enrolled.
-        final boolean awakeKeyguardExcludingBouncerShowing = mKeyguardIsVisible
-                && mDeviceInteractive && !mGoingToSleep
-                && !statusBarShadeLocked && !mBouncerIsOrWillBeShowing;
+        final boolean awakeKeyguard = mKeyguardIsVisible && mDeviceInteractive && !mGoingToSleep
+                && !statusBarShadeLocked;
         final int user = getCurrentUser();
         final int strongAuth = mStrongAuthTracker.getStrongAuthForUser(user);
         final boolean isLockDown =
@@ -2637,16 +2634,15 @@
         final boolean faceDisabledForUser = isFaceDisabled(user);
         final boolean biometricEnabledForUser = mBiometricEnabledForUser.get(user);
         final boolean shouldListenForFaceAssistant = shouldListenForFaceAssistant();
-        final boolean onlyFaceEnrolled = isOnlyFaceEnrolled();
         final boolean fpOrFaceIsLockedOut = isFaceLockedOut() || fpLockedout;
 
         // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an
         // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware.
         final boolean shouldListen =
-                ((mBouncerFullyShown && !mGoingToSleep && onlyFaceEnrolled)
+                (mBouncerFullyShown && !mGoingToSleep
                         || mAuthInterruptActive
                         || mOccludingAppRequestingFace
-                        || awakeKeyguardExcludingBouncerShowing
+                        || awakeKeyguard
                         || shouldListenForFaceAssistant
                         || mAuthController.isUdfpsFingerDown()
                         || mUdfpsBouncerShowing)
@@ -2667,17 +2663,15 @@
                     becauseCannotSkipBouncer,
                     biometricEnabledForUser,
                     mBouncerFullyShown,
-                    mBouncerIsOrWillBeShowing,
                     faceAuthenticated,
                     faceDisabledForUser,
                     isFaceLockedOut(),
                     fpLockedout,
                     mGoingToSleep,
-                    awakeKeyguardExcludingBouncerShowing,
+                    awakeKeyguard,
                     mKeyguardGoingAway,
                     shouldListenForFaceAssistant,
                     mOccludingAppRequestingFace,
-                    onlyFaceEnrolled,
                     mIsPrimaryUser,
                     strongAuthAllowsScanning,
                     mSecureCameraLaunched,
@@ -2687,11 +2681,6 @@
         return shouldListen;
     }
 
-    private boolean isOnlyFaceEnrolled() {
-        return isFaceEnrolled()
-                && !getCachedIsUnlockWithFingerprintPossible(sCurrentUser);
-    }
-
     private void maybeLogListenerModelData(KeyguardListenModel model) {
         mLogger.logKeyguardListenerModel(model);
 
@@ -3243,8 +3232,7 @@
                     cb.onKeyguardBouncerStateChanged(mBouncerIsOrWillBeShowing);
                 }
             }
-            updateBiometricListeningState(BIOMETRIC_ACTION_UPDATE,
-                    FaceAuthUiEvent.FACE_AUTH_UPDATED_PRIMARY_BOUNCER_SHOWN_OR_WILL_BE_SHOWN);
+            updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE);
         }
 
         if (wasBouncerFullyShown != mBouncerFullyShown) {
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 7fc8123..a5fdc68 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -103,7 +103,6 @@
 import com.android.systemui.statusbar.phone.SystemUIDialogManager;
 import com.android.systemui.statusbar.policy.AccessibilityController;
 import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
-import com.android.systemui.statusbar.policy.BatteryController;
 import com.android.systemui.statusbar.policy.BluetoothController;
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -253,7 +252,6 @@
     @Inject Lazy<UserInfoController> mUserInfoController;
     @Inject Lazy<KeyguardStateController> mKeyguardMonitor;
     @Inject Lazy<KeyguardUpdateMonitor> mKeyguardUpdateMonitor;
-    @Inject Lazy<BatteryController> mBatteryController;
     @Inject Lazy<NightDisplayListener> mNightDisplayListener;
     @Inject Lazy<ReduceBrightColorsController> mReduceBrightColorsController;
     @Inject Lazy<ManagedProfileController> mManagedProfileController;
@@ -404,8 +402,6 @@
 
         mProviders.put(UserInfoController.class, mUserInfoController::get);
 
-        mProviders.put(BatteryController.class, mBatteryController::get);
-
         mProviders.put(NightDisplayListener.class, mNightDisplayListener::get);
 
         mProviders.put(ReduceBrightColorsController.class, mReduceBrightColorsController::get);
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
index bfbf37a..d53e56f 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/BrightLineFalsingManager.java
@@ -220,7 +220,7 @@
             return r;
         }).collect(Collectors.toList());
 
-        logDebug("False Gesture: " + localResult[0]);
+        logDebug("False Gesture (type: " + interactionType + "): " + localResult[0]);
 
         return localResult[0];
     }
@@ -454,6 +454,12 @@
         }
     }
 
+    static void logVerbose(String msg) {
+        if (DEBUG) {
+            Log.v(TAG, msg);
+        }
+    }
+
     static void logInfo(String msg) {
         Log.i(TAG, msg);
         RECENT_INFO_LOG.add(msg);
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
index f18413b..c292296 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/Classifier.java
@@ -42,8 +42,9 @@
     public static final int QS_COLLAPSE = 12;
     public static final int UDFPS_AUTHENTICATION = 13;
     public static final int LOCK_ICON = 14;
-    public static final int QS_SWIPE = 15;
+    public static final int QS_SWIPE_SIDE = 15;
     public static final int BACK_GESTURE = 16;
+    public static final int QS_SWIPE_NESTED = 17;
 
     @IntDef({
             QUICK_SETTINGS,
@@ -62,7 +63,8 @@
             BRIGHTNESS_SLIDER,
             UDFPS_AUTHENTICATION,
             LOCK_ICON,
-            QS_SWIPE,
+            QS_SWIPE_SIDE,
+            QS_SWIPE_NESTED,
             BACK_GESTURE
     })
     @Retention(RetentionPolicy.SOURCE)
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
index d0fe1c3..5e4f149 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/DistanceClassifier.java
@@ -24,6 +24,7 @@
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_DISTANCE_VERTICAL_SWIPE_THRESHOLD_IN;
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
 import static com.android.systemui.classifier.Classifier.SHADE_DRAG;
 
 import android.provider.DeviceConfig;
@@ -156,7 +157,8 @@
                 || interactionType == QS_COLLAPSE
                 || interactionType == Classifier.UDFPS_AUTHENTICATION
                 || interactionType == Classifier.LOCK_ICON
-                || interactionType == Classifier.QS_SWIPE) {
+                || interactionType == Classifier.QS_SWIPE_SIDE
+                || interactionType == QS_SWIPE_NESTED) {
             return Result.passed(0);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingClassifier.java
index d757528..d18d62f 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingClassifier.java
@@ -148,6 +148,11 @@
     }
 
     /** */
+    public static void logVerbose(String msg) {
+        BrightLineFalsingManager.logVerbose(msg);
+    }
+
+    /** */
     public static void logInfo(String msg) {
         BrightLineFalsingManager.logInfo(msg);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
index a3ecb0c..3991a35 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/FalsingDataProvider.java
@@ -78,10 +78,10 @@
 
     void onMotionEvent(MotionEvent motionEvent) {
         List<MotionEvent> motionEvents = unpackMotionEvent(motionEvent);
-        FalsingClassifier.logDebug("Unpacked into: " + motionEvents.size());
+        FalsingClassifier.logVerbose("Unpacked into: " + motionEvents.size());
         if (BrightLineFalsingManager.DEBUG) {
             for (MotionEvent m : motionEvents) {
-                FalsingClassifier.logDebug(
+                FalsingClassifier.logVerbose(
                         "x,y,t: " + m.getX() + "," + m.getY() + "," + m.getEventTime());
             }
         }
@@ -92,7 +92,7 @@
         }
         mRecentMotionEvents.addAll(motionEvents);
 
-        FalsingClassifier.logDebug("Size: " + mRecentMotionEvents.size());
+        FalsingClassifier.logVerbose("Size: " + mRecentMotionEvents.size());
 
         mMotionEventListeners.forEach(listener -> listener.onMotionEvent(motionEvent));
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
index 32d9ca59..07f94e7 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/ProximityClassifier.java
@@ -19,7 +19,7 @@
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.BRIGHTLINE_FALSING_PROXIMITY_PERCENT_COVERED_THRESHOLD;
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 
 import android.provider.DeviceConfig;
@@ -119,7 +119,7 @@
             @Classifier.InteractionType int interactionType,
             double historyBelief, double historyConfidence) {
         if (interactionType == QUICK_SETTINGS || interactionType == BRIGHTNESS_SLIDER
-                || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE) {
+                || interactionType == QS_COLLAPSE || interactionType == QS_SWIPE_SIDE) {
             return Result.passed(0);
         }
 
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
index f040712..776bc88 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/TypeClassifier.java
@@ -24,7 +24,8 @@
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN;
 import static com.android.systemui.classifier.Classifier.PULSE_EXPAND;
 import static com.android.systemui.classifier.Classifier.QS_COLLAPSE;
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;
 import static com.android.systemui.classifier.Classifier.SHADE_DRAG;
@@ -86,9 +87,12 @@
             case QS_COLLAPSE:
                 wrongDirection = !vertical || !up;
                 break;
-            case QS_SWIPE:
+            case QS_SWIPE_SIDE:
                 wrongDirection = vertical;
                 break;
+            case QS_SWIPE_NESTED:
+                wrongDirection = !vertical;
+                break;
             default:
                 wrongDirection = true;
                 break;
diff --git a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
index 40c28fa..de2bdf7 100644
--- a/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
+++ b/packages/SystemUI/src/com/android/systemui/classifier/ZigZagClassifier.java
@@ -137,8 +137,8 @@
             runningAbsDy += Math.abs(point.y - pY);
             pX = point.x;
             pY = point.y;
-            logDebug("(x, y, runningAbsDx, runningAbsDy) - (" + pX + ", " + pY + ", " + runningAbsDx
-                    + ", " + runningAbsDy + ")");
+            logVerbose("(x, y, runningAbsDx, runningAbsDy) - ("
+                    + pX + ", " + pY + ", " + runningAbsDx + ", " + runningAbsDy + ")");
         }
 
         float devianceX = runningAbsDx - actualDx;
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
index fbfc94a..a996699 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java
@@ -35,6 +35,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerImpl;
 import com.android.systemui.doze.DozeHost;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.media.dagger.MediaModule;
 import com.android.systemui.navigationbar.gestural.GestureModule;
 import com.android.systemui.plugins.qs.QSFactory;
@@ -126,6 +127,7 @@
             PowerManager powerManager,
             BroadcastDispatcher broadcastDispatcher,
             DemoModeController demoModeController,
+            DumpManager dumpManager,
             @Main Handler mainHandler,
             @Background Handler bgHandler) {
         BatteryController bC = new BatteryControllerImpl(
@@ -134,6 +136,7 @@
                 powerManager,
                 broadcastDispatcher,
                 demoModeController,
+                dumpManager,
                 mainHandler,
                 bgHandler);
         bC.init();
diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
index 0469152..443d277 100644
--- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java
@@ -41,6 +41,7 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.flags.FlagsModule;
 import com.android.systemui.fragments.FragmentService;
+import com.android.systemui.keyguard.data.BouncerViewModule;
 import com.android.systemui.log.dagger.LogModule;
 import com.android.systemui.media.dagger.MediaProjectionModule;
 import com.android.systemui.model.SysUiState;
@@ -116,6 +117,7 @@
             AppOpsModule.class,
             AssistModule.class,
             BiometricsModule.class,
+            BouncerViewModule.class,
             ClockModule.class,
             CoroutinesModule.class,
             DreamModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index d7b7777..733a80d 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -35,6 +35,7 @@
 import com.android.systemui.dreams.complication.ComplicationHostViewController;
 import com.android.systemui.dreams.dagger.DreamOverlayComponent;
 import com.android.systemui.dreams.dagger.DreamOverlayModule;
+import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
 import com.android.systemui.statusbar.BlurUtils;
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
 import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
@@ -73,6 +74,7 @@
     // Main thread handler used to schedule periodic tasks (e.g. burn-in protection updates).
     private final Handler mHandler;
     private final int mDreamOverlayMaxTranslationY;
+    private final BouncerCallbackInteractor mBouncerCallbackInteractor;
 
     private long mJitterStartTimeMillis;
 
@@ -131,7 +133,8 @@
             @Named(DreamOverlayModule.MAX_BURN_IN_OFFSET) int maxBurnInOffset,
             @Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long
                     burnInProtectionUpdateInterval,
-            @Named(DreamOverlayModule.MILLIS_UNTIL_FULL_JITTER) long millisUntilFullJitter) {
+            @Named(DreamOverlayModule.MILLIS_UNTIL_FULL_JITTER) long millisUntilFullJitter,
+            BouncerCallbackInteractor bouncerCallbackInteractor) {
         super(containerView);
         mDreamOverlayContentView = contentView;
         mStatusBarViewController = statusBarViewController;
@@ -151,6 +154,7 @@
         mMaxBurnInOffset = maxBurnInOffset;
         mBurnInProtectionUpdateInterval = burnInProtectionUpdateInterval;
         mMillisUntilFullJitter = millisUntilFullJitter;
+        mBouncerCallbackInteractor = bouncerCallbackInteractor;
     }
 
     @Override
@@ -167,6 +171,7 @@
         if (bouncer != null) {
             bouncer.addBouncerExpansionCallback(mBouncerExpansionCallback);
         }
+        mBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback);
     }
 
     @Override
@@ -176,6 +181,7 @@
         if (bouncer != null) {
             bouncer.removeBouncerExpansionCallback(mBouncerExpansionCallback);
         }
+        mBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
     }
 
     View getContainerView() {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 96f77b3..696fc72 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.dreams;
 
+import android.content.ComponentName;
 import android.content.Context;
 import android.graphics.drawable.ColorDrawable;
 import android.util.Log;
@@ -26,11 +27,13 @@
 import android.view.WindowManager;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.annotation.VisibleForTesting;
 import androidx.lifecycle.Lifecycle;
 import androidx.lifecycle.LifecycleRegistry;
 import androidx.lifecycle.ViewModelStore;
 
+import com.android.dream.lowlight.dagger.LowLightDreamModule;
 import com.android.internal.logging.UiEvent;
 import com.android.internal.logging.UiEventLogger;
 import com.android.internal.policy.PhoneWindow;
@@ -44,6 +47,7 @@
 import java.util.concurrent.Executor;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 
 /**
  * The {@link DreamOverlayService} is responsible for placing an overlay on top of a dream. The
@@ -62,6 +66,8 @@
     // content area).
     private final DreamOverlayContainerViewController mDreamOverlayContainerViewController;
     private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    @Nullable
+    private final ComponentName mLowLightDreamComponent;
     private final UiEventLogger mUiEventLogger;
 
     // A reference to the {@link Window} used to hold the dream overlay.
@@ -125,10 +131,13 @@
             DreamOverlayComponent.Factory dreamOverlayComponentFactory,
             DreamOverlayStateController stateController,
             KeyguardUpdateMonitor keyguardUpdateMonitor,
-            UiEventLogger uiEventLogger) {
+            UiEventLogger uiEventLogger,
+            @Nullable @Named(LowLightDreamModule.LOW_LIGHT_DREAM_COMPONENT)
+                    ComponentName lowLightDreamComponent) {
         mContext = context;
         mExecutor = executor;
         mKeyguardUpdateMonitor = keyguardUpdateMonitor;
+        mLowLightDreamComponent = lowLightDreamComponent;
         mKeyguardUpdateMonitor.registerCallback(mKeyguardCallback);
         mStateController = stateController;
         mUiEventLogger = uiEventLogger;
@@ -155,6 +164,7 @@
             windowManager.removeView(mWindow.getDecorView());
         }
         mStateController.setOverlayActive(false);
+        mStateController.setLowLightActive(false);
         mDestroyed = true;
         super.onDestroy();
     }
@@ -163,6 +173,9 @@
     public void onStartDream(@NonNull WindowManager.LayoutParams layoutParams) {
         mUiEventLogger.log(DreamOverlayEvent.DREAM_OVERLAY_ENTER_START);
         setCurrentState(Lifecycle.State.STARTED);
+        final ComponentName dreamComponent = getDreamComponent();
+        mStateController.setLowLightActive(
+                dreamComponent != null && dreamComponent.equals(mLowLightDreamComponent));
         mExecutor.execute(() -> {
             if (mDestroyed) {
                 // The task could still be executed after the service has been destroyed. Bail if
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index 69e41ba..72feaca 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -50,6 +50,7 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     public static final int STATE_DREAM_OVERLAY_ACTIVE = 1 << 0;
+    public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1;
 
     private static final int OP_CLEAR_STATE = 1;
     private static final int OP_SET_STATE = 2;
@@ -193,6 +194,14 @@
         return containsState(STATE_DREAM_OVERLAY_ACTIVE);
     }
 
+    /**
+     * Returns whether low light mode is active.
+     * @return {@code true} if in low light mode, {@code false} otherwise.
+     */
+    public boolean isLowLightActive() {
+        return containsState(STATE_LOW_LIGHT_ACTIVE);
+    }
+
     private boolean containsState(int state) {
         return (mState & state) != 0;
     }
@@ -222,6 +231,14 @@
     }
 
     /**
+     * Sets whether low light mode is active.
+     * @param active {@code true} if low light mode is active, {@code false} otherwise.
+     */
+    public void setLowLightActive(boolean active) {
+        modifyState(active ? OP_SET_STATE : OP_CLEAR_STATE, STATE_LOW_LIGHT_ACTIVE);
+    }
+
+    /**
      * Returns the available complication types.
      */
     @Complication.ComplicationType
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
index aa59cc6..bb1c430 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -20,7 +20,6 @@
 import static android.app.StatusBarManager.WINDOW_STATE_HIDING;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
 
-import android.annotation.Nullable;
 import android.app.AlarmManager;
 import android.app.StatusBarManager;
 import android.content.res.Resources;
@@ -36,6 +35,8 @@
 import android.util.PluralsMessageFormatter;
 import android.view.View;
 
+import androidx.annotation.Nullable;
+
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.dreams.DreamOverlayStatusBarItemsProvider.StatusBarItem;
@@ -73,6 +74,8 @@
     private final Optional<DreamOverlayNotificationCountProvider>
             mDreamOverlayNotificationCountProvider;
     private final ZenModeController mZenModeController;
+    private final DreamOverlayStateController mDreamOverlayStateController;
+    private final StatusBarWindowStateController mStatusBarWindowStateController;
     private final DreamOverlayStatusBarItemsProvider mStatusBarItemsProvider;
     private final Executor mMainExecutor;
     private final List<DreamOverlayStatusBarItemsProvider.StatusBarItem> mExtraStatusBarItems =
@@ -102,6 +105,14 @@
         }
     };
 
+    private final DreamOverlayStateController.Callback mDreamOverlayStateCallback =
+            new DreamOverlayStateController.Callback() {
+                @Override
+                public void onStateChanged() {
+                    updateLowLightState();
+                }
+            };
+
     private final IndividualSensorPrivacyController.Callback mSensorCallback =
             (sensor, blocked) -> updateMicCameraBlockedStatusIcon();
 
@@ -140,7 +151,8 @@
             Optional<DreamOverlayNotificationCountProvider> dreamOverlayNotificationCountProvider,
             ZenModeController zenModeController,
             StatusBarWindowStateController statusBarWindowStateController,
-            DreamOverlayStatusBarItemsProvider statusBarItemsProvider) {
+            DreamOverlayStatusBarItemsProvider statusBarItemsProvider,
+            DreamOverlayStateController dreamOverlayStateController) {
         super(view);
         mResources = resources;
         mMainExecutor = mainExecutor;
@@ -151,8 +163,10 @@
         mDateFormatUtil = dateFormatUtil;
         mSensorPrivacyController = sensorPrivacyController;
         mDreamOverlayNotificationCountProvider = dreamOverlayNotificationCountProvider;
+        mStatusBarWindowStateController = statusBarWindowStateController;
         mStatusBarItemsProvider = statusBarItemsProvider;
         mZenModeController = zenModeController;
+        mDreamOverlayStateController = dreamOverlayStateController;
 
         // Register to receive show/hide updates for the system status bar. Our custom status bar
         // needs to hide when the system status bar is showing to ovoid overlapping status bars.
@@ -180,6 +194,9 @@
 
         mStatusBarItemsProvider.addCallback(mStatusBarItemsProviderCallback);
 
+        mDreamOverlayStateController.addCallback(mDreamOverlayStateCallback);
+        updateLowLightState();
+
         mTouchInsetSession.addViewToTracking(mView);
     }
 
@@ -193,6 +210,7 @@
                 provider -> provider.removeCallback(mNotificationCountCallback));
         mStatusBarItemsProvider.removeCallback(mStatusBarItemsProviderCallback);
         mView.removeAllExtraStatusBarItemViews();
+        mDreamOverlayStateController.removeCallback(mDreamOverlayStateCallback);
         mTouchInsetSession.clear();
 
         mIsAttached = false;
@@ -217,6 +235,15 @@
                 hasAlarm ? buildAlarmContentDescription(alarm) : null);
     }
 
+    private void updateLowLightState() {
+        int visibility = View.VISIBLE;
+        if (mDreamOverlayStateController.isLowLightActive()
+                || mStatusBarWindowStateController.windowIsShowing()) {
+            visibility = View.INVISIBLE;
+        }
+        mView.setVisibility(visibility);
+    }
+
     private String buildAlarmContentDescription(AlarmManager.AlarmClockInfo alarm) {
         final String skeleton = mDateFormatUtil.is24HourFormat() ? "EHm" : "Ehma";
         final String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), skeleton);
@@ -272,7 +299,7 @@
 
     private void onSystemStatusBarStateChanged(@StatusBarManager.WindowVisibleState int state) {
         mMainExecutor.execute(() -> {
-            if (!mIsAttached) {
+            if (!mIsAttached || mDreamOverlayStateController.isLowLightActive()) {
                 return;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
index 21a51d1..c07d402 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/DreamMediaEntryComplication.java
@@ -18,13 +18,21 @@
 
 import static com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent.DreamMediaEntryModule.DREAM_MEDIA_ENTRY_VIEW;
 import static com.android.systemui.dreams.complication.dagger.RegisteredComplicationsModule.DREAM_MEDIA_ENTRY_LAYOUT_PARAMS;
+import static com.android.systemui.flags.Flags.DREAM_MEDIA_TAP_TO_OPEN;
 
+import android.app.PendingIntent;
 import android.util.Log;
 import android.view.View;
 
+import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.dreams.complication.dagger.DreamMediaEntryComplicationComponent;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.media.MediaCarouselController;
 import com.android.systemui.media.dream.MediaDreamComplication;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
 import com.android.systemui.util.ViewController;
 
 import javax.inject.Inject;
@@ -87,6 +95,15 @@
 
         private final DreamOverlayStateController mDreamOverlayStateController;
         private final MediaDreamComplication mMediaComplication;
+        private final MediaCarouselController mMediaCarouselController;
+
+        private final ActivityStarter mActivityStarter;
+        private final ActivityIntentHelper mActivityIntentHelper;
+        private final KeyguardStateController mKeyguardStateController;
+        private final NotificationLockscreenUserManager mLockscreenUserManager;
+
+        private final FeatureFlags mFeatureFlags;
+        private boolean mIsTapToOpenEnabled;
 
         private boolean mMediaComplicationAdded;
 
@@ -94,15 +111,28 @@
         DreamMediaEntryViewController(
                 @Named(DREAM_MEDIA_ENTRY_VIEW) View view,
                 DreamOverlayStateController dreamOverlayStateController,
-                MediaDreamComplication mediaComplication) {
+                MediaDreamComplication mediaComplication,
+                MediaCarouselController mediaCarouselController,
+                ActivityStarter activityStarter,
+                ActivityIntentHelper activityIntentHelper,
+                KeyguardStateController keyguardStateController,
+                NotificationLockscreenUserManager lockscreenUserManager,
+                FeatureFlags featureFlags) {
             super(view);
             mDreamOverlayStateController = dreamOverlayStateController;
             mMediaComplication = mediaComplication;
+            mMediaCarouselController = mediaCarouselController;
+            mActivityStarter = activityStarter;
+            mActivityIntentHelper = activityIntentHelper;
+            mKeyguardStateController = keyguardStateController;
+            mLockscreenUserManager = lockscreenUserManager;
+            mFeatureFlags = featureFlags;
             mView.setOnClickListener(this::onClickMediaEntry);
         }
 
         @Override
         protected void onViewAttached() {
+            mIsTapToOpenEnabled = mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN);
         }
 
         @Override
@@ -113,6 +143,31 @@
         private void onClickMediaEntry(View v) {
             if (DEBUG) Log.d(TAG, "media entry complication tapped");
 
+            if (mIsTapToOpenEnabled) {
+                final PendingIntent clickIntent =
+                        mMediaCarouselController.getCurrentVisibleMediaContentIntent();
+
+                if (clickIntent == null) {
+                    return;
+                }
+
+                // See StatusBarNotificationActivityStarter#onNotificationClicked
+                final boolean showOverLockscreen = mKeyguardStateController.isShowing()
+                        && mActivityIntentHelper.wouldShowOverLockscreen(clickIntent.getIntent(),
+                        mLockscreenUserManager.getCurrentUserId());
+
+                if (showOverLockscreen) {
+                    mActivityStarter.startActivity(clickIntent.getIntent(),
+                            /* dismissShade */ true,
+                            /* animationController */ null,
+                            /* showOverLockscreenWhenLocked */ true);
+                } else {
+                    mActivityStarter.postStartActivityDismissingKeyguard(clickIntent, null);
+                }
+
+                return;
+            }
+
             if (!mMediaComplicationAdded) {
                 addMediaComplication();
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
index 2dd2098..f9dca08 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.content.res.Resources;
 
+import com.android.dream.lowlight.dagger.LowLightDreamModule;
 import com.android.settingslib.dream.DreamBackend;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -37,6 +38,7 @@
  */
 @Module(includes = {
             RegisteredComplicationsModule.class,
+            LowLightDreamModule.class,
         },
         subcomponents = {
             DreamOverlayComponent.class,
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.java b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
index 030c7ce..48f5f9e 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.java
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.java
@@ -104,6 +104,10 @@
     public static final UnreleasedFlag MODERN_USER_SWITCHER_ACTIVITY =
             new UnreleasedFlag(209, true);
 
+    /** Whether the new implementation of UserSwitcherController should be used. */
+    public static final UnreleasedFlag REFACTORED_USER_SWITCHER_CONTROLLER =
+            new UnreleasedFlag(210, false);
+
     /***************************************/
     // 300 - power menu
     public static final ReleasedFlag POWER_MENU_LITE =
@@ -196,7 +200,8 @@
     public static final UnreleasedFlag MEDIA_SESSION_ACTIONS = new UnreleasedFlag(901);
     public static final ReleasedFlag MEDIA_NEARBY_DEVICES = new ReleasedFlag(903);
     public static final ReleasedFlag MEDIA_MUTE_AWAIT = new ReleasedFlag(904);
-    public static final UnreleasedFlag MEDIA_DREAM_COMPLICATION = new UnreleasedFlag(905);
+    public static final UnreleasedFlag DREAM_MEDIA_COMPLICATION = new UnreleasedFlag(905);
+    public static final UnreleasedFlag DREAM_MEDIA_TAP_TO_OPEN = new UnreleasedFlag(906);
 
     // 1000 - dock
     public static final ReleasedFlag SIMULATE_DOCK_THROUGH_CHARGING =
@@ -238,6 +243,10 @@
     public static final SysPropBooleanFlag FLOATING_TASKS_ENABLED =
             new SysPropBooleanFlag(1106, "persist.wm.debug.floating_tasks", false);
 
+    @Keep
+    public static final SysPropBooleanFlag SHOW_FLOATING_TASKS_AS_BUBBLES =
+            new SysPropBooleanFlag(1107, "persist.wm.debug.floating_tasks_as_bubbles", false);
+
     // 1200 - predictive back
     @Keep
     public static final SysPropBooleanFlag WM_ENABLE_PREDICTIVE_BACK = new SysPropBooleanFlag(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 4214240..38b98eb 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -22,6 +22,7 @@
 import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER;
 
 import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NAV_BAR_HANDLE_SHOW_OVER_LOCKSCREEN;
+import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_OCCLUSION;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_TRANSITION_FROM_AOD;
 import static com.android.internal.jank.InteractionJankMonitor.CUJ_LOCKSCREEN_UNLOCK_ANIMATION;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED;
@@ -845,6 +846,8 @@
                     if (launchIsFullScreen) {
                         mCentralSurfaces.instantCollapseNotificationPanel();
                     }
+
+                    mInteractionJankMonitor.end(CUJ_LOCKSCREEN_OCCLUSION);
                 }
 
                 @NonNull
@@ -991,6 +994,8 @@
                     setOccluded(isKeyguardOccluded /* isOccluded */, false /* animate */);
                     Log.d(TAG, "Unocclude animation cancelled. Occluded state is now: "
                             + mOccluded);
+
+                    mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_OCCLUSION);
                 }
 
                 @Override
@@ -999,6 +1004,9 @@
                         RemoteAnimationTarget[] nonApps,
                         IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
                     Log.d(TAG, "UnoccludeAnimator#onAnimationStart. Set occluded = false.");
+                    mInteractionJankMonitor.begin(
+                            createInteractionJankMonitorConf(CUJ_LOCKSCREEN_OCCLUSION)
+                                    .setTag("UNOCCLUDE"));
                     setOccluded(false /* isOccluded */, true /* animate */);
 
                     if (apps == null || apps.length == 0 || apps[0] == null) {
@@ -1057,6 +1065,8 @@
                                 try {
                                     finishedCallback.onAnimationFinished();
                                     mUnoccludeAnimator = null;
+
+                                    mInteractionJankMonitor.end(CUJ_LOCKSCREEN_OCCLUSION);
                                 } catch (RemoteException e) {
                                     e.printStackTrace();
                                 }
@@ -2573,7 +2583,8 @@
                         };
                 try {
                     mInteractionJankMonitor.begin(
-                            createInteractionJankMonitorConf("RunRemoteAnimation"));
+                            createInteractionJankMonitorConf(
+                                    CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "RunRemoteAnimation"));
                     runner.onAnimationStart(WindowManager.TRANSIT_KEYGUARD_GOING_AWAY, apps,
                             wallpapers, nonApps, callback);
                 } catch (RemoteException e) {
@@ -2589,7 +2600,8 @@
                 mSurfaceBehindRemoteAnimationRunning = true;
 
                 mInteractionJankMonitor.begin(
-                        createInteractionJankMonitorConf("DismissPanel"));
+                        createInteractionJankMonitorConf(
+                                CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "DismissPanel"));
 
                 // Pass the surface and metadata to the unlock animation controller.
                 mKeyguardUnlockAnimationControllerLazy.get()
@@ -2597,7 +2609,8 @@
                                 apps, startTime, mSurfaceBehindRemoteAnimationRequested);
             } else {
                 mInteractionJankMonitor.begin(
-                        createInteractionJankMonitorConf("RemoteAnimationDisabled"));
+                        createInteractionJankMonitorConf(
+                                CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "RemoteAnimationDisabled"));
 
                 mKeyguardViewControllerLazy.get().hide(startTime, fadeoutDuration);
 
@@ -2677,10 +2690,15 @@
         sendUserPresentBroadcast();
     }
 
-    private Configuration.Builder createInteractionJankMonitorConf(String tag) {
-        return Configuration.Builder.withView(CUJ_LOCKSCREEN_UNLOCK_ANIMATION,
-                mKeyguardViewControllerLazy.get().getViewRootImpl().getView())
-                .setTag(tag);
+    private Configuration.Builder createInteractionJankMonitorConf(int cuj) {
+        return createInteractionJankMonitorConf(cuj, null /* tag */);
+    }
+
+    private Configuration.Builder createInteractionJankMonitorConf(int cuj, @Nullable String tag) {
+        final Configuration.Builder builder = Configuration.Builder.withView(
+                cuj, mKeyguardViewControllerLazy.get().getViewRootImpl().getView());
+
+        return tag != null ? builder.setTag(tag) : builder;
     }
 
     /**
@@ -3291,6 +3309,10 @@
                 IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
             super.onAnimationStart(transit, apps, wallpapers, nonApps, finishedCallback);
 
+            mInteractionJankMonitor.begin(
+                    createInteractionJankMonitorConf(CUJ_LOCKSCREEN_OCCLUSION)
+                            .setTag("OCCLUDE"));
+
             // This is the first signal we have from WM that we're going to be occluded. Set our
             // internal state to reflect that immediately, vs. waiting for the launch animator to
             // begin. Otherwise, calls to setShowingLocked, etc. will not know that we're about to
@@ -3307,6 +3329,7 @@
                     + "Setting occluded state to: " + isKeyguardOccluded);
             setOccluded(isKeyguardOccluded /* occluded */, false /* animate */);
 
+            mInteractionJankMonitor.cancel(CUJ_LOCKSCREEN_OCCLUSION);
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
new file mode 100644
index 0000000..99ae85d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.data
+
+import android.view.KeyEvent
+import com.android.systemui.dagger.SysUISingleton
+import java.lang.ref.WeakReference
+import javax.inject.Inject
+
+/** An abstraction to interface with the ui layer, without changing state. */
+interface BouncerView {
+    var delegate: BouncerViewDelegate?
+}
+
+/** A lightweight class to hold reference to the ui delegate. */
+@SysUISingleton
+class BouncerViewImpl @Inject constructor() : BouncerView {
+    private var _delegate: WeakReference<BouncerViewDelegate?> = WeakReference(null)
+    override var delegate: BouncerViewDelegate?
+        get() = _delegate.get()
+        set(value) {
+            _delegate = WeakReference(value)
+        }
+}
+
+/** An abstraction that implements view logic. */
+interface BouncerViewDelegate {
+    fun isFullScreenBouncer(): Boolean
+    fun shouldDismissOnMenuPressed(): Boolean
+    fun interceptMediaKey(event: KeyEvent?): Boolean
+    fun dispatchBackKeyEventPreIme(): Boolean
+    fun showNextSecurityScreenOrFinish(): Boolean
+    fun resume()
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerViewModule.kt
similarity index 60%
copy from packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
copy to packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerViewModule.kt
index 6e17214..390c54e 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerViewModule.kt
@@ -14,15 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.systemui.compose.gallery
+package com.android.systemui.keyguard.data
 
-import androidx.compose.foundation.layout.Column
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.android.systemui.ExampleFeature
+import dagger.Binds
+import dagger.Module
 
-/** The screen that shows ExampleFeature. */
-@Composable
-fun ExampleFeatureScreen(modifier: Modifier = Modifier) {
-    Column(modifier) { ExampleFeature("This is an example feature!") }
+@Module
+interface BouncerViewModule {
+    /** Binds BouncerView to BouncerViewImpl and makes it injectable. */
+    @Binds fun bindBouncerView(bouncerViewImpl: BouncerViewImpl): BouncerView
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
new file mode 100644
index 0000000..543389e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -0,0 +1,154 @@
+/*
+ * 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 android.hardware.biometrics.BiometricSourceType
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
+import com.android.keyguard.ViewMediatorCallback
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel
+import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Encapsulates app state for the lock screen bouncer. */
+@SysUISingleton
+class KeyguardBouncerRepository
+@Inject
+constructor(
+    private val viewMediatorCallback: ViewMediatorCallback,
+    keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) {
+    var bouncerPromptReason: Int? = null
+    /** Determines if we want to instantaneously show the bouncer instead of translating. */
+    private val _isScrimmed = MutableStateFlow(false)
+    val isScrimmed = _isScrimmed.asStateFlow()
+    /** Set amount of how much of the bouncer is showing on the screen */
+    private val _expansionAmount = MutableStateFlow(EXPANSION_HIDDEN)
+    val expansionAmount = _expansionAmount.asStateFlow()
+    private val _isVisible = MutableStateFlow(false)
+    val isVisible = _isVisible.asStateFlow()
+    private val _show = MutableStateFlow<KeyguardBouncerModel?>(null)
+    val show = _show.asStateFlow()
+    private val _showingSoon = MutableStateFlow(false)
+    val showingSoon = _showingSoon.asStateFlow()
+    private val _hide = MutableStateFlow(false)
+    val hide = _hide.asStateFlow()
+    private val _startingToHide = MutableStateFlow(false)
+    val startingToHide = _startingToHide.asStateFlow()
+    private val _onDismissAction = MutableStateFlow<BouncerCallbackActionsModel?>(null)
+    val onDismissAction = _onDismissAction.asStateFlow()
+    private val _disappearAnimation = MutableStateFlow<Runnable?>(null)
+    val startingDisappearAnimation = _disappearAnimation.asStateFlow()
+    private val _keyguardPosition = MutableStateFlow(0f)
+    val keyguardPosition = _keyguardPosition.asStateFlow()
+    private val _resourceUpdateRequests = MutableStateFlow(false)
+    val resourceUpdateRequests = _resourceUpdateRequests.asStateFlow()
+    private val _showMessage = MutableStateFlow<BouncerShowMessageModel?>(null)
+    val showMessage = _showMessage.asStateFlow()
+    private val _keyguardAuthenticated = MutableStateFlow<Boolean?>(null)
+    /** Determines if user is already unlocked */
+    val keyguardAuthenticated = _keyguardAuthenticated.asStateFlow()
+    private val _isBackButtonEnabled = MutableStateFlow<Boolean?>(null)
+    val isBackButtonEnabled = _isBackButtonEnabled.asStateFlow()
+    private val _onScreenTurnedOff = MutableStateFlow(false)
+    val onScreenTurnedOff = _onScreenTurnedOff.asStateFlow()
+
+    val bouncerErrorMessage: CharSequence?
+        get() = viewMediatorCallback.consumeCustomMessage()
+
+    init {
+        val callback =
+            object : KeyguardUpdateMonitorCallback() {
+                override fun onStrongAuthStateChanged(userId: Int) {
+                    bouncerPromptReason = viewMediatorCallback.bouncerPromptReason
+                }
+
+                override fun onLockedOutStateChanged(type: BiometricSourceType) {
+                    if (type == BiometricSourceType.FINGERPRINT) {
+                        bouncerPromptReason = viewMediatorCallback.bouncerPromptReason
+                    }
+                }
+            }
+
+        keyguardUpdateMonitor.registerCallback(callback)
+    }
+
+    fun setScrimmed(isScrimmed: Boolean) {
+        _isScrimmed.value = isScrimmed
+    }
+
+    fun setExpansion(expansion: Float) {
+        _expansionAmount.value = expansion
+    }
+
+    fun setVisible(isVisible: Boolean) {
+        _isVisible.value = isVisible
+    }
+
+    fun setShow(keyguardBouncerModel: KeyguardBouncerModel?) {
+        _show.value = keyguardBouncerModel
+    }
+
+    fun setShowingSoon(showingSoon: Boolean) {
+        _showingSoon.value = showingSoon
+    }
+
+    fun setHide(hide: Boolean) {
+        _hide.value = hide
+    }
+
+    fun setStartingToHide(startingToHide: Boolean) {
+        _startingToHide.value = startingToHide
+    }
+
+    fun setOnDismissAction(bouncerCallbackActionsModel: BouncerCallbackActionsModel?) {
+        _onDismissAction.value = bouncerCallbackActionsModel
+    }
+
+    fun setStartDisappearAnimation(runnable: Runnable?) {
+        _disappearAnimation.value = runnable
+    }
+
+    fun setKeyguardPosition(keyguardPosition: Float) {
+        _keyguardPosition.value = keyguardPosition
+    }
+
+    fun setResourceUpdateRequests(willUpdateResources: Boolean) {
+        _resourceUpdateRequests.value = willUpdateResources
+    }
+
+    fun setShowMessage(bouncerShowMessageModel: BouncerShowMessageModel?) {
+        _showMessage.value = bouncerShowMessageModel
+    }
+
+    fun setKeyguardAuthenticated(keyguardAuthenticated: Boolean?) {
+        _keyguardAuthenticated.value = keyguardAuthenticated
+    }
+
+    fun setIsBackButtonEnabled(isBackButtonEnabled: Boolean) {
+        _isBackButtonEnabled.value = isBackButtonEnabled
+    }
+
+    fun setOnScreenTurnedOff(onScreenTurnedOff: Boolean) {
+        _onScreenTurnedOff.value = onScreenTurnedOff
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractor.kt
new file mode 100644
index 0000000..10c7a37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractor.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.interactor
+
+import android.view.View
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.util.ListenerSet
+import javax.inject.Inject
+
+/** Interactor to add and remove callbacks for the bouncer. */
+@SysUISingleton
+class BouncerCallbackInteractor @Inject constructor() {
+    private var resetCallbacks = ListenerSet<KeyguardBouncer.KeyguardResetCallback>()
+    private var expansionCallbacks = ArrayList<KeyguardBouncer.BouncerExpansionCallback>()
+    /** Add a KeyguardResetCallback. */
+    fun addKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
+        resetCallbacks.addIfAbsent(callback)
+    }
+
+    /** Remove a KeyguardResetCallback. */
+    fun removeKeyguardResetCallback(callback: KeyguardBouncer.KeyguardResetCallback) {
+        resetCallbacks.remove(callback)
+    }
+
+    /** Adds a callback to listen to bouncer expansion updates. */
+    fun addBouncerExpansionCallback(callback: KeyguardBouncer.BouncerExpansionCallback) {
+        if (!expansionCallbacks.contains(callback)) {
+            expansionCallbacks.add(callback)
+        }
+    }
+
+    /**
+     * Removes a previously added callback. If the callback was never added, this method does
+     * nothing.
+     */
+    fun removeBouncerExpansionCallback(callback: KeyguardBouncer.BouncerExpansionCallback) {
+        expansionCallbacks.remove(callback)
+    }
+
+    /** Propagate fully shown to bouncer expansion callbacks. */
+    fun dispatchFullyShown() {
+        for (callback in expansionCallbacks) {
+            callback.onFullyShown()
+        }
+    }
+
+    /** Propagate starting to hide to bouncer expansion callbacks. */
+    fun dispatchStartingToHide() {
+        for (callback in expansionCallbacks) {
+            callback.onStartingToHide()
+        }
+    }
+
+    /** Propagate starting to show to bouncer expansion callbacks. */
+    fun dispatchStartingToShow() {
+        for (callback in expansionCallbacks) {
+            callback.onStartingToShow()
+        }
+    }
+
+    /** Propagate fully hidden to bouncer expansion callbacks. */
+    fun dispatchFullyHidden() {
+        for (callback in expansionCallbacks) {
+            callback.onFullyHidden()
+        }
+    }
+
+    /** Propagate expansion changes to bouncer expansion callbacks. */
+    fun dispatchExpansionChanged(expansion: Float) {
+        for (callback in expansionCallbacks) {
+            callback.onExpansionChanged(expansion)
+        }
+    }
+    /** Propagate visibility changes to bouncer expansion callbacks. */
+    fun dispatchVisibilityChanged(visibility: Int) {
+        for (callback in expansionCallbacks) {
+            callback.onVisibilityChanged(visibility == View.VISIBLE)
+        }
+    }
+
+    /** Propagate keyguard reset. */
+    fun dispatchReset() {
+        for (callback in resetCallbacks) {
+            callback.onKeyguardReset()
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
new file mode 100644
index 0000000..7d4db37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
@@ -0,0 +1,324 @@
+/*
+ * 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.interactor
+
+import android.content.res.ColorStateList
+import android.os.Handler
+import android.os.Trace
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.DejankUtils
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel
+import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.shared.system.SysUiStatsLog
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+/** Encapsulates business logic for interacting with the lock-screen bouncer. */
+@SysUISingleton
+class BouncerInteractor
+@Inject
+constructor(
+    private val repository: KeyguardBouncerRepository,
+    private val bouncerView: BouncerView,
+    @Main private val mainHandler: Handler,
+    private val keyguardStateController: KeyguardStateController,
+    private val keyguardSecurityModel: KeyguardSecurityModel,
+    private val callbackInteractor: BouncerCallbackInteractor,
+    private val falsingCollector: FalsingCollector,
+    private val dismissCallbackRegistry: DismissCallbackRegistry,
+    keyguardBypassController: KeyguardBypassController,
+    keyguardUpdateMonitor: KeyguardUpdateMonitor,
+) {
+    /** Whether we want to wait for face auth. */
+    private val bouncerFaceDelay =
+        keyguardStateController.isFaceAuthEnabled &&
+            !keyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
+                KeyguardUpdateMonitor.getCurrentUser()
+            ) &&
+            !needsFullscreenBouncer() &&
+            !keyguardUpdateMonitor.userNeedsStrongAuth() &&
+            !keyguardBypassController.bypassEnabled
+
+    /** Runnable to show the bouncer. */
+    val showRunnable = Runnable {
+        repository.setVisible(true)
+        repository.setShow(
+            KeyguardBouncerModel(
+                promptReason = repository.bouncerPromptReason ?: 0,
+                errorMessage = repository.bouncerErrorMessage,
+                expansionAmount = repository.expansionAmount.value
+            )
+        )
+        repository.setShowingSoon(false)
+    }
+
+    val keyguardAuthenticated: Flow<Boolean> = repository.keyguardAuthenticated.filterNotNull()
+    val screenTurnedOff: Flow<Unit> = repository.onScreenTurnedOff.filter { it }.map {}
+    val show: Flow<KeyguardBouncerModel> = repository.show.filterNotNull()
+    val hide: Flow<Unit> = repository.hide.filter { it }.map {}
+    val startingToHide: Flow<Unit> = repository.startingToHide.filter { it }.map {}
+    val isVisible: Flow<Boolean> = repository.isVisible
+    val isBackButtonEnabled: Flow<Boolean> = repository.isBackButtonEnabled.filterNotNull()
+    val expansionAmount: Flow<Float> = repository.expansionAmount
+    val showMessage: Flow<BouncerShowMessageModel> = repository.showMessage.filterNotNull()
+    val startingDisappearAnimation: Flow<Runnable> =
+        repository.startingDisappearAnimation.filterNotNull()
+    val onDismissAction: Flow<BouncerCallbackActionsModel> =
+        repository.onDismissAction.filterNotNull()
+    val resourceUpdateRequests: Flow<Boolean> = repository.resourceUpdateRequests.filter { it }
+    val keyguardPosition: Flow<Float> = repository.keyguardPosition
+
+    // TODO(b/243685699): Move isScrimmed logic to data layer.
+    // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
+    /** Show the bouncer if necessary and set the relevant states. */
+    @JvmOverloads
+    fun show(isScrimmed: Boolean) {
+        // Reset some states as we show the bouncer.
+        repository.setShowMessage(null)
+        repository.setOnScreenTurnedOff(false)
+        repository.setKeyguardAuthenticated(null)
+        repository.setHide(false)
+        repository.setStartingToHide(false)
+
+        val resumeBouncer =
+            (repository.isVisible.value || repository.showingSoon.value) && needsFullscreenBouncer()
+
+        if (!resumeBouncer && repository.show.value != null) {
+            // If bouncer is visible, the bouncer is already showing.
+            return
+        }
+
+        val keyguardUserId = KeyguardUpdateMonitor.getCurrentUser()
+        if (keyguardUserId == UserHandle.USER_SYSTEM && UserManager.isSplitSystemUser()) {
+            // In split system user mode, we never unlock system user.
+            return
+        }
+
+        Trace.beginSection("KeyguardBouncer#show")
+        repository.setScrimmed(isScrimmed)
+        if (isScrimmed) {
+            setExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
+        }
+
+        if (resumeBouncer) {
+            bouncerView.delegate?.resume()
+            // Bouncer is showing the next security screen and we just need to prompt a resume.
+            return
+        }
+        if (bouncerView.delegate?.showNextSecurityScreenOrFinish() == true) {
+            // Keyguard is done.
+            return
+        }
+
+        repository.setShowingSoon(true)
+        if (bouncerFaceDelay) {
+            mainHandler.postDelayed(showRunnable, 1200L)
+        } else {
+            DejankUtils.postAfterTraversal(showRunnable)
+        }
+        keyguardStateController.notifyBouncerShowing(true)
+        callbackInteractor.dispatchStartingToShow()
+
+        Trace.endSection()
+    }
+
+    /** Sets the correct bouncer states to hide the bouncer. */
+    fun hide() {
+        Trace.beginSection("KeyguardBouncer#hide")
+        if (isFullyShowing()) {
+            SysUiStatsLog.write(
+                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED,
+                SysUiStatsLog.KEYGUARD_BOUNCER_STATE_CHANGED__STATE__HIDDEN
+            )
+            dismissCallbackRegistry.notifyDismissCancelled()
+        }
+
+        falsingCollector.onBouncerHidden()
+        keyguardStateController.notifyBouncerShowing(false /* showing */)
+        cancelShowRunnable()
+        repository.setShowingSoon(false)
+        repository.setOnDismissAction(null)
+        repository.setVisible(false)
+        repository.setHide(true)
+        repository.setShow(null)
+        Trace.endSection()
+    }
+
+    /**
+     * Sets the panel expansion which is calculated further upstream. Expansion is from 0f to 1f
+     * where 0f => showing and 1f => hiding
+     */
+    fun setExpansion(expansion: Float) {
+        val oldExpansion = repository.expansionAmount.value
+        val expansionChanged = oldExpansion != expansion
+        if (repository.startingDisappearAnimation.value == null) {
+            repository.setExpansion(expansion)
+        }
+
+        if (
+            expansion == KeyguardBouncer.EXPANSION_VISIBLE &&
+                oldExpansion != KeyguardBouncer.EXPANSION_VISIBLE
+        ) {
+            falsingCollector.onBouncerShown()
+            callbackInteractor.dispatchFullyShown()
+        } else if (
+            expansion == KeyguardBouncer.EXPANSION_HIDDEN &&
+                oldExpansion != KeyguardBouncer.EXPANSION_HIDDEN
+        ) {
+            repository.setVisible(false)
+            repository.setShow(null)
+            falsingCollector.onBouncerHidden()
+            DejankUtils.postAfterTraversal { callbackInteractor.dispatchReset() }
+            callbackInteractor.dispatchFullyHidden()
+        } else if (
+            expansion != KeyguardBouncer.EXPANSION_VISIBLE &&
+                oldExpansion == KeyguardBouncer.EXPANSION_VISIBLE
+        ) {
+            callbackInteractor.dispatchStartingToHide()
+            repository.setStartingToHide(true)
+        }
+        if (expansionChanged) {
+            callbackInteractor.dispatchExpansionChanged(expansion)
+        }
+    }
+
+    /** Set the initial keyguard message to show when bouncer is shown. */
+    fun showMessage(message: String?, colorStateList: ColorStateList?) {
+        repository.setShowMessage(BouncerShowMessageModel(message, colorStateList))
+    }
+
+    /**
+     * Sets actions to the bouncer based on how the bouncer is dismissed. If the bouncer is
+     * unlocked, we will run the onDismissAction. If the bouncer is existed before unlocking, we
+     * call cancelAction.
+     */
+    fun setDismissAction(
+        onDismissAction: ActivityStarter.OnDismissAction?,
+        cancelAction: Runnable?
+    ) {
+        repository.setOnDismissAction(BouncerCallbackActionsModel(onDismissAction, cancelAction))
+    }
+
+    /** Update the resources of the views. */
+    fun updateResources() {
+        repository.setResourceUpdateRequests(true)
+    }
+
+    /** Tell the bouncer that keyguard is authenticated. */
+    fun notifyKeyguardAuthenticated(strongAuth: Boolean) {
+        repository.setKeyguardAuthenticated(strongAuth)
+    }
+
+    /** Tell the bouncer the screen has turned off. */
+    fun onScreenTurnedOff() {
+        repository.setOnScreenTurnedOff(true)
+    }
+
+    /** Update the position of the bouncer when showing. */
+    fun setKeyguardPosition(position: Float) {
+        repository.setKeyguardPosition(position)
+    }
+
+    /** Notifies that the state change was handled. */
+    fun notifyKeyguardAuthenticatedHandled() {
+        repository.setKeyguardAuthenticated(null)
+    }
+
+    /** Notify that view visibility has changed. */
+    fun notifyBouncerVisibilityHasChanged(visibility: Int) {
+        callbackInteractor.dispatchVisibilityChanged(visibility)
+    }
+
+    /** Notify that the resources have been updated */
+    fun notifyUpdatedResources() {
+        repository.setResourceUpdateRequests(false)
+    }
+
+    /** Set whether back button is enabled when on the bouncer screen. */
+    fun setBackButtonEnabled(enabled: Boolean) {
+        repository.setIsBackButtonEnabled(enabled)
+    }
+
+    /** Tell the bouncer to start the pre hide animation. */
+    fun startDisappearAnimation(runnable: Runnable) {
+        val finishRunnable = Runnable {
+            repository.setStartDisappearAnimation(null)
+            runnable.run()
+        }
+        repository.setStartDisappearAnimation(finishRunnable)
+    }
+
+    /** Returns whether bouncer is fully showing. */
+    fun isFullyShowing(): Boolean {
+        return (repository.showingSoon.value || repository.isVisible.value) &&
+            repository.expansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE &&
+            repository.startingDisappearAnimation.value == null
+    }
+
+    /** Returns whether bouncer is scrimmed. */
+    fun isScrimmed(): Boolean {
+        return repository.isScrimmed.value
+    }
+
+    /** If bouncer expansion is between 0f and 1f non-inclusive. */
+    fun isInTransit(): Boolean {
+        return repository.showingSoon.value ||
+            repository.expansionAmount.value != KeyguardBouncer.EXPANSION_HIDDEN &&
+                repository.expansionAmount.value != KeyguardBouncer.EXPANSION_VISIBLE
+    }
+
+    /** Return whether bouncer is animating away. */
+    fun isAnimatingAway(): Boolean {
+        return repository.startingDisappearAnimation.value != null
+    }
+
+    /** Return whether bouncer will dismiss with actions */
+    fun willDismissWithAction(): Boolean {
+        return repository.onDismissAction.value?.onDismissAction != null
+    }
+
+    /** Returns whether the bouncer should be full screen. */
+    private fun needsFullscreenBouncer(): Boolean {
+        val mode: KeyguardSecurityModel.SecurityMode =
+            keyguardSecurityModel.getSecurityMode(KeyguardUpdateMonitor.getCurrentUser())
+        return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
+            mode == KeyguardSecurityModel.SecurityMode.SimPuk
+    }
+
+    /** Remove the show runnable from the main handler queue to improve performance. */
+    private fun cancelShowRunnable() {
+        DejankUtils.removeCallbacks(showRunnable)
+        mainHandler.removeCallbacks(showRunnable)
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerCallbackActionsModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerCallbackActionsModel.kt
new file mode 100644
index 0000000..81cf5b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerCallbackActionsModel.kt
@@ -0,0 +1,26 @@
+/*
+ * 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 com.android.systemui.plugins.ActivityStarter
+
+/** Encapsulates callbacks to be invoked by the bouncer logic. */
+// TODO(b/243683121): Move dismiss logic from view controllers
+data class BouncerCallbackActionsModel(
+    val onDismissAction: ActivityStarter.OnDismissAction?,
+    val cancelAction: Runnable?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerShowMessageModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerShowMessageModel.kt
new file mode 100644
index 0000000..05cdeaa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BouncerShowMessageModel.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package com.android.systemui.keyguard.shared.model
+
+import android.content.res.ColorStateList
+
+/** Show a keyguard message to the bouncer. */
+data class BouncerShowMessageModel(val message: String?, val colorStateList: ColorStateList?)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBouncerModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBouncerModel.kt
new file mode 100644
index 0000000..ad783da
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardBouncerModel.kt
@@ -0,0 +1,24 @@
+/*
+ * 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
+
+/** Models the state of the lock-screen bouncer */
+data class KeyguardBouncerModel(
+    val promptReason: Int = 0,
+    val errorMessage: CharSequence? = null,
+    val expansionAmount: Float = 0f,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
new file mode 100644
index 0000000..df26014
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -0,0 +1,220 @@
+/*
+ * 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.view.KeyEvent
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.internal.policy.SystemBarUtils
+import com.android.keyguard.KeyguardHostViewController
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.dagger.KeyguardBouncerComponent
+import com.android.systemui.keyguard.data.BouncerViewDelegate
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
+
+/** Binds the bouncer container to its view model. */
+object KeyguardBouncerViewBinder {
+    @JvmStatic
+    fun bind(
+        view: ViewGroup,
+        viewModel: KeyguardBouncerViewModel,
+        componentFactory: KeyguardBouncerComponent.Factory
+    ) {
+        // Builds the KeyguardHostViewController from bouncer view group.
+        val hostViewController: KeyguardHostViewController =
+            componentFactory.create(view).keyguardHostViewController
+        hostViewController.init()
+        val delegate =
+            object : BouncerViewDelegate {
+                override fun isFullScreenBouncer(): Boolean {
+                    val mode = hostViewController.currentSecurityMode
+                    return mode == KeyguardSecurityModel.SecurityMode.SimPin ||
+                        mode == KeyguardSecurityModel.SecurityMode.SimPuk
+                }
+
+                override fun shouldDismissOnMenuPressed(): Boolean {
+                    return hostViewController.shouldEnableMenuKey()
+                }
+
+                override fun interceptMediaKey(event: KeyEvent?): Boolean {
+                    return hostViewController.interceptMediaKey(event)
+                }
+
+                override fun dispatchBackKeyEventPreIme(): Boolean {
+                    return hostViewController.dispatchBackKeyEventPreIme()
+                }
+
+                override fun showNextSecurityScreenOrFinish(): Boolean {
+                    return hostViewController.dismiss(KeyguardUpdateMonitor.getCurrentUser())
+                }
+
+                override fun resume() {
+                    hostViewController.showPrimarySecurityScreen()
+                    hostViewController.onResume()
+                }
+            }
+        view.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                try {
+                    viewModel.setBouncerViewDelegate(delegate)
+                    launch {
+                        viewModel.show.collect {
+                            hostViewController.showPrimarySecurityScreen()
+                            hostViewController.appear(
+                                SystemBarUtils.getStatusBarHeight(view.context)
+                            )
+                        }
+                    }
+
+                    launch {
+                        viewModel.showPromptReason.collect { prompt ->
+                            hostViewController.showPromptReason(prompt)
+                        }
+                    }
+
+                    launch {
+                        viewModel.showBouncerErrorMessage.collect { errorMessage ->
+                            hostViewController.showErrorMessage(errorMessage)
+                        }
+                    }
+
+                    launch {
+                        viewModel.showWithFullExpansion.collect { model ->
+                            hostViewController.resetSecurityContainer()
+                            hostViewController.showPromptReason(model.promptReason)
+                            hostViewController.onResume()
+                        }
+                    }
+
+                    launch {
+                        viewModel.hide.collect {
+                            hostViewController.cancelDismissAction()
+                            hostViewController.cleanUp()
+                            hostViewController.resetSecurityContainer()
+                        }
+                    }
+
+                    launch {
+                        viewModel.startingToHide.collect { hostViewController.onStartingToHide() }
+                    }
+
+                    launch {
+                        viewModel.setDismissAction.collect {
+                            hostViewController.setOnDismissAction(
+                                it.onDismissAction,
+                                it.cancelAction
+                            )
+                        }
+                    }
+
+                    launch {
+                        viewModel.startDisappearAnimation.collect {
+                            hostViewController.startDisappearAnimation(it)
+                        }
+                    }
+
+                    launch {
+                        viewModel.bouncerExpansionAmount.collect { expansion ->
+                            hostViewController.setExpansion(expansion)
+                        }
+                    }
+
+                    launch {
+                        viewModel.bouncerExpansionAmount
+                            .filter { it == EXPANSION_VISIBLE }
+                            .collect {
+                                hostViewController.onResume()
+                                view.announceForAccessibility(
+                                    hostViewController.accessibilityTitleForCurrentMode
+                                )
+                            }
+                    }
+
+                    launch {
+                        viewModel.isBouncerVisible.collect { isVisible ->
+                            val visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
+                            view.visibility = visibility
+                            hostViewController.onBouncerVisibilityChanged(visibility)
+                            viewModel.notifyBouncerVisibilityHasChanged(visibility)
+                        }
+                    }
+
+                    launch {
+                        viewModel.isBouncerVisible
+                            .filter { !it }
+                            .collect {
+                                // Remove existing input for security reasons.
+                                hostViewController.resetSecurityContainer()
+                            }
+                    }
+
+                    launch {
+                        viewModel.keyguardPosition.collect { position ->
+                            hostViewController.updateKeyguardPosition(position)
+                        }
+                    }
+
+                    launch {
+                        viewModel.updateResources.collect {
+                            hostViewController.updateResources()
+                            viewModel.notifyUpdateResources()
+                        }
+                    }
+
+                    launch {
+                        viewModel.bouncerShowMessage.collect {
+                            hostViewController.showMessage(it.message, it.colorStateList)
+                        }
+                    }
+
+                    launch {
+                        viewModel.keyguardAuthenticated.collect {
+                            hostViewController.finish(it, KeyguardUpdateMonitor.getCurrentUser())
+                            viewModel.notifyKeyguardAuthenticated()
+                        }
+                    }
+
+                    launch {
+                        viewModel
+                            .observeOnIsBackButtonEnabled { view.systemUiVisibility }
+                            .collect { view.systemUiVisibility = it }
+                    }
+
+                    launch {
+                        viewModel.screenTurnedOff.collect {
+                            if (view.visibility == View.VISIBLE) {
+                                hostViewController.onPause()
+                            }
+                        }
+                    }
+                    awaitCancellation()
+                } finally {
+                    viewModel.setBouncerViewDelegate(null)
+                }
+            }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
new file mode 100644
index 0000000..9ad5211
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.view.View
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.BouncerViewDelegate
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor
+import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel
+import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+/** Models UI state for the lock screen bouncer; handles user input. */
+class KeyguardBouncerViewModel
+@Inject
+constructor(
+    private val view: BouncerView,
+    private val interactor: BouncerInteractor,
+) {
+    /** Observe on bouncer expansion amount. */
+    val bouncerExpansionAmount: Flow<Float> = interactor.expansionAmount
+
+    /** Observe on bouncer visibility. */
+    val isBouncerVisible: Flow<Boolean> = interactor.isVisible
+
+    /** Observe whether bouncer is showing. */
+    val show: Flow<KeyguardBouncerModel> = interactor.show
+
+    /** Observe bouncer prompt when bouncer is showing. */
+    val showPromptReason: Flow<Int> = interactor.show.map { it.promptReason }
+
+    /** Observe bouncer error message when bouncer is showing. */
+    val showBouncerErrorMessage: Flow<CharSequence> =
+        interactor.show.map { it.errorMessage }.filterNotNull()
+
+    /** Observe visible expansion when bouncer is showing. */
+    val showWithFullExpansion: Flow<KeyguardBouncerModel> =
+        interactor.show.filter { it.expansionAmount == EXPANSION_VISIBLE }
+
+    /** Observe whether bouncer is hiding. */
+    val hide: Flow<Unit> = interactor.hide
+
+    /** Observe whether bouncer is starting to hide. */
+    val startingToHide: Flow<Unit> = interactor.startingToHide
+
+    /** Observe whether we want to set the dismiss action to the bouncer. */
+    val setDismissAction: Flow<BouncerCallbackActionsModel> = interactor.onDismissAction
+
+    /** Observe whether we want to start the disappear animation. */
+    val startDisappearAnimation: Flow<Runnable> = interactor.startingDisappearAnimation
+
+    /** Observe whether we want to update keyguard position. */
+    val keyguardPosition: Flow<Float> = interactor.keyguardPosition
+
+    /** Observe whether we want to update resources. */
+    val updateResources: Flow<Boolean> = interactor.resourceUpdateRequests
+
+    /** Observe whether we want to set a keyguard message when the bouncer shows. */
+    val bouncerShowMessage: Flow<BouncerShowMessageModel> = interactor.showMessage
+
+    /** Observe whether keyguard is authenticated already. */
+    val keyguardAuthenticated: Flow<Boolean> = interactor.keyguardAuthenticated
+
+    /** Observe whether screen is turned off. */
+    val screenTurnedOff: Flow<Unit> = interactor.screenTurnedOff
+
+    /** Notify that view visibility has changed. */
+    fun notifyBouncerVisibilityHasChanged(visibility: Int) {
+        return interactor.notifyBouncerVisibilityHasChanged(visibility)
+    }
+    /** Observe whether we want to update resources. */
+    fun notifyUpdateResources() {
+        interactor.notifyUpdatedResources()
+    }
+
+    /** Notify that keyguard authenticated was handled */
+    fun notifyKeyguardAuthenticated() {
+        interactor.notifyKeyguardAuthenticatedHandled()
+    }
+
+    /** Observe whether back button is enabled. */
+    fun observeOnIsBackButtonEnabled(systemUiVisibility: () -> Int): Flow<Int> {
+        return interactor.isBackButtonEnabled.map { enabled ->
+            var vis: Int = systemUiVisibility()
+            vis =
+                if (enabled) {
+                    vis and View.STATUS_BAR_DISABLE_BACK.inv()
+                } else {
+                    vis or View.STATUS_BAR_DISABLE_BACK
+                }
+            vis
+        }
+    }
+
+    /** Set an abstraction that will hold reference to the ui delegate for the bouncer view. */
+    fun setBouncerViewDelegate(delegate: BouncerViewDelegate?) {
+        view.delegate = delegate
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
index e25f5da..f8c6a57 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaCarouselController.kt
@@ -1,5 +1,6 @@
 package com.android.systemui.media
 
+import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
 import android.content.res.ColorStateList
@@ -945,6 +946,11 @@
         mediaManager.onSwipeToDismiss()
     }
 
+    fun getCurrentVisibleMediaContentIntent(): PendingIntent? {
+        return MediaPlayerData.playerKeys()
+                .elementAtOrNull(mediaCarouselScrollHandler.visibleMediaIndex)?.data?.clickIntent
+    }
+
     override fun dump(pw: PrintWriter, args: Array<out String>) {
         pw.apply {
             println("keysNeedRemoval: $keysNeedRemoval")
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
index 267c1f5..b3a4ddf 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaDeviceManager.kt
@@ -188,24 +188,28 @@
 
         @AnyThread
         fun start() = bgExecutor.execute {
-            localMediaManager.registerCallback(this)
-            localMediaManager.startScan()
-            muteAwaitConnectionManager?.startListening()
-            playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
-            controller?.registerCallback(this)
-            updateCurrent()
-            started = true
-            configurationController.addCallback(configListener)
+            if (!started) {
+                localMediaManager.registerCallback(this)
+                localMediaManager.startScan()
+                muteAwaitConnectionManager?.startListening()
+                playbackType = controller?.playbackInfo?.playbackType ?: PLAYBACK_TYPE_UNKNOWN
+                controller?.registerCallback(this)
+                updateCurrent()
+                started = true
+                configurationController.addCallback(configListener)
+            }
         }
 
         @AnyThread
         fun stop() = bgExecutor.execute {
-            started = false
-            controller?.unregisterCallback(this)
-            localMediaManager.stopScan()
-            localMediaManager.unregisterCallback(this)
-            muteAwaitConnectionManager?.stopListening()
-            configurationController.removeCallback(configListener)
+            if (started) {
+                started = false
+                controller?.unregisterCallback(this)
+                localMediaManager.stopScan()
+                localMediaManager.unregisterCallback(this)
+                muteAwaitConnectionManager?.stopListening()
+                configurationController.removeCallback(configListener)
+            }
         }
 
         fun dump(pw: PrintWriter) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
index 0f1ee31..c6bd777 100644
--- a/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/MediaProjectionAppSelectorActivity.kt
@@ -24,22 +24,45 @@
 import android.os.IBinder
 import android.os.ResultReceiver
 import android.os.UserHandle
-import android.widget.ImageView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.internal.annotations.VisibleForTesting
 import com.android.internal.app.ChooserActivity
 import com.android.internal.app.ResolverListController
 import com.android.internal.app.chooser.NotSelectableTargetInfo
 import com.android.internal.app.chooser.TargetInfo
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.util.AsyncActivityLauncher
 import com.android.systemui.R
-import com.android.internal.R as AndroidR
+import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
+import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorView
+import com.android.systemui.mediaprojection.appselector.data.RecentTask
+import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter
+import com.android.systemui.mediaprojection.appselector.view.RecentTasksAdapter.RecentTaskClickListener
+import com.android.systemui.util.AsyncActivityLauncher
+import com.android.systemui.util.recycler.HorizontalSpacerItemDecoration
+import javax.inject.Inject
 
-class MediaProjectionAppSelectorActivity constructor(
+class MediaProjectionAppSelectorActivity(
     private val activityLauncher: AsyncActivityLauncher,
+    private val controller: MediaProjectionAppSelectorController,
+    private val recentTasksAdapterFactory: RecentTasksAdapter.Factory,
     /** This is used to override the dependency in a screenshot test */
     @VisibleForTesting
-    private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)? = null
-) : ChooserActivity() {
+    private val listControllerFactory: ((userHandle: UserHandle) -> ResolverListController)?
+) : ChooserActivity(), MediaProjectionAppSelectorView, RecentTaskClickListener {
+
+    @Inject
+    constructor(
+        activityLauncher: AsyncActivityLauncher,
+        controller: MediaProjectionAppSelectorController,
+        recentTasksAdapterFactory: RecentTasksAdapter.Factory,
+    ) : this(activityLauncher, controller, recentTasksAdapterFactory, null)
+
+    private var recentsRoot: ViewGroup? = null
+    private var recentsProgress: View? = null
+    private var recentsRecycler: RecyclerView? = null
 
     override fun getLayoutResource() =
         R.layout.media_projection_app_selector
@@ -52,10 +75,30 @@
         // TODO(b/240939253): update copies
         val title = getString(R.string.media_projection_dialog_service_title)
         intent.putExtra(Intent.EXTRA_TITLE, title)
-
         super.onCreate(bundle)
+        controller.init(this)
+    }
 
-        requireViewById<ImageView>(AndroidR.id.icon).setImageResource(R.drawable.ic_present_to_all)
+    private fun createRecentsView(parent: ViewGroup): ViewGroup {
+        val recentsRoot = LayoutInflater.from(this)
+            .inflate(R.layout.media_projection_recent_tasks, parent,
+                    /* attachToRoot= */ false) as ViewGroup
+
+        recentsProgress = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_loader)
+        recentsRecycler = recentsRoot.requireViewById(R.id.media_projection_recent_tasks_recycler)
+        recentsRecycler?.layoutManager = LinearLayoutManager(
+            this, LinearLayoutManager.HORIZONTAL,
+            /* reverseLayout= */false
+        )
+
+        val itemDecoration = HorizontalSpacerItemDecoration(
+            resources.getDimensionPixelOffset(
+                R.dimen.media_projection_app_selector_recents_padding
+            )
+        )
+        recentsRecycler?.addItemDecoration(itemDecoration)
+
+        return recentsRoot
     }
 
     override fun appliedThemeResId(): Int =
@@ -108,6 +151,7 @@
 
     override fun onDestroy() {
         activityLauncher.destroy()
+        controller.destroy()
         super.onDestroy()
     }
 
@@ -115,6 +159,27 @@
         // do nothing
     }
 
+    override fun bind(recentTasks: List<RecentTask>) {
+        val recents = recentsRoot ?: return
+        val progress = recentsProgress ?: return
+        val recycler = recentsRecycler ?: return
+
+        if (recentTasks.isEmpty()) {
+            recents.visibility = View.GONE
+            return
+        }
+
+        progress.visibility = View.GONE
+        recycler.visibility = View.VISIBLE
+        recents.visibility = View.VISIBLE
+
+        recycler.adapter = recentTasksAdapterFactory.create(recentTasks, this)
+    }
+
+    override fun onRecentClicked(task: RecentTask, view: View) {
+        // TODO(b/240924732) Handle clicking on a recent task
+    }
+
     private fun onTargetActivityLaunched(launchToken: IBinder) {
         if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) {
             // The client requested to return the result in the result receiver instead of
@@ -145,6 +210,14 @@
 
     override fun shouldGetOnlyDefaultActivities() = false
 
+    // TODO(b/240924732) flip the flag when the recents selector is ready
+    override fun shouldShowContentPreview() = false
+
+    override fun createContentPreviewView(parent: ViewGroup): ViewGroup =
+            recentsRoot ?: createRecentsView(parent).also {
+                recentsRoot = it
+            }
+
     companion object {
         /**
          * When EXTRA_CAPTURE_REGION_RESULT_RECEIVER is passed as intent extra
diff --git a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
index 9696998..185b4fc 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/dagger/MediaProjectionModule.kt
@@ -17,13 +17,29 @@
 package com.android.systemui.media.dagger
 
 import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.media.MediaProjectionAppSelectorActivity
-import com.android.systemui.util.AsyncActivityLauncher
+import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorController
+import com.android.systemui.mediaprojection.appselector.data.ActivityTaskManagerThumbnailLoader
+import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
+import com.android.systemui.mediaprojection.appselector.data.IconLoaderLibAppIconLoader
+import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
+import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
+import com.android.systemui.mediaprojection.appselector.data.ShellRecentTaskListProvider
 import dagger.Binds
 import dagger.Module
 import dagger.Provides
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import javax.inject.Qualifier
+
+@Qualifier
+@Retention(AnnotationRetention.BINARY)
+annotation class MediaProjectionAppSelector
 
 @Module
 abstract class MediaProjectionModule {
@@ -31,17 +47,43 @@
     @Binds
     @IntoMap
     @ClassKey(MediaProjectionAppSelectorActivity::class)
-    abstract fun bindMediaProjectionAppSelectorActivity(
-        activity: MediaProjectionAppSelectorActivity): Activity
+    abstract fun provideMediaProjectionAppSelectorActivity(
+        activity: MediaProjectionAppSelectorActivity
+    ): Activity
+
+    @Binds
+    abstract fun bindRecentTaskThumbnailLoader(
+        impl: ActivityTaskManagerThumbnailLoader
+    ): RecentTaskThumbnailLoader
+
+    @Binds
+    abstract fun bindRecentTaskListProvider(
+        impl: ShellRecentTaskListProvider
+    ): RecentTaskListProvider
+
+    @Binds
+    abstract fun bindAppIconLoader(impl: IconLoaderLibAppIconLoader): AppIconLoader
 
     companion object {
         @Provides
-        fun provideMediaProjectionAppSelectorActivity(
-            activityLauncher: AsyncActivityLauncher
-        ): MediaProjectionAppSelectorActivity {
-            return MediaProjectionAppSelectorActivity(
-                activityLauncher
+        fun provideController(
+            recentTaskListProvider: RecentTaskListProvider,
+            context: Context,
+            @MediaProjectionAppSelector scope: CoroutineScope
+        ): MediaProjectionAppSelectorController {
+            val appSelectorComponentName =
+                ComponentName(context, MediaProjectionAppSelectorActivity::class.java)
+
+            return MediaProjectionAppSelectorController(
+                recentTaskListProvider,
+                scope,
+                appSelectorComponentName
             )
         }
+
+        @MediaProjectionAppSelector
+        @Provides
+        fun provideCoroutineScope(@Application applicationScope: CoroutineScope): CoroutineScope =
+            CoroutineScope(applicationScope.coroutineContext + SupervisorJob())
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
index 85d8f3f..a9e1a4d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputBaseDialog.java
@@ -99,6 +99,7 @@
     private Button mStopButton;
     private Button mAppButton;
     private int mListMaxHeight;
+    private int mItemHeight;
     private WallpaperColors mWallpaperColors;
     private Executor mExecutor;
     private boolean mShouldLaunchLeBroadcastDialog;
@@ -106,10 +107,12 @@
     MediaOutputBaseAdapter mAdapter;
 
     private final ViewTreeObserver.OnGlobalLayoutListener mDeviceListLayoutListener = () -> {
+        ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
+        int totalItemsHeight = mAdapter.getItemCount() * mItemHeight;
+        int correctHeight = Math.min(totalItemsHeight, mListMaxHeight);
         // Set max height for list
-        if (mDeviceListLayout.getHeight() > mListMaxHeight) {
-            ViewGroup.LayoutParams params = mDeviceListLayout.getLayoutParams();
-            params.height = mListMaxHeight;
+        if (correctHeight != params.height) {
+            params.height = correctHeight;
             mDeviceListLayout.setLayoutParams(params);
         }
     };
@@ -212,6 +215,8 @@
         mLayoutManager = new LayoutManagerWrapper(mContext);
         mListMaxHeight = context.getResources().getDimensionPixelSize(
                 R.dimen.media_output_dialog_list_max_height);
+        mItemHeight = context.getResources().getDimensionPixelSize(
+                R.dimen.media_output_dialog_list_item_height);
         mExecutor = Executors.newSingleThreadExecutor();
     }
 
@@ -246,8 +251,10 @@
         mDeviceListLayout.getViewTreeObserver().addOnGlobalLayoutListener(
                 mDeviceListLayoutListener);
         // Init device list
+        mLayoutManager.setAutoMeasureEnabled(true);
         mDevicesRecyclerView.setLayoutManager(mLayoutManager);
         mDevicesRecyclerView.setAdapter(mAdapter);
+        mDevicesRecyclerView.setHasFixedSize(false);
         // Init header icon
         mHeaderIcon.setOnClickListener(v -> onHeaderIconClick());
         // Init bottom buttons
diff --git a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
index 7b4ac12..19b401d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputController.java
@@ -125,13 +125,16 @@
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager;
     private final Map<String, Integer> mNearbyDeviceInfoMap = new ConcurrentHashMap<>();
 
-    private boolean mIsRefreshing = false;
-    private boolean mNeedRefresh = false;
+    @VisibleForTesting
+    boolean mIsRefreshing = false;
+    @VisibleForTesting
+    boolean mNeedRefresh = false;
     private MediaController mMediaController;
     @VisibleForTesting
     Callback mCallback;
     @VisibleForTesting
     LocalMediaManager mLocalMediaManager;
+    @VisibleForTesting
     private MediaOutputMetricLogger mMetricLogger;
     private int mCurrentState;
 
@@ -221,15 +224,7 @@
                 Log.d(TAG, "No media controller for " + mPackageName);
             }
         }
-        if (mLocalMediaManager == null) {
-            if (DEBUG) {
-                Log.d(TAG, "No local media manager " + mPackageName);
-            }
-            return;
-        }
         mCallback = cb;
-        mLocalMediaManager.unregisterCallback(this);
-        mLocalMediaManager.stopScan();
         mLocalMediaManager.registerCallback(this);
         mLocalMediaManager.startScan();
     }
@@ -251,10 +246,8 @@
         if (mMediaController != null) {
             mMediaController.unregisterCallback(mCb);
         }
-        if (mLocalMediaManager != null) {
-            mLocalMediaManager.unregisterCallback(this);
-            mLocalMediaManager.stopScan();
-        }
+        mLocalMediaManager.unregisterCallback(this);
+        mLocalMediaManager.stopScan();
         synchronized (mMediaDevicesLock) {
             mCachedMediaDevices.clear();
             mMediaDevices.clear();
@@ -658,10 +651,6 @@
         return mLocalMediaManager.getCurrentConnectedDevice();
     }
 
-    private MediaDevice getMediaDeviceById(String id) {
-        return mLocalMediaManager.getMediaDeviceById(new ArrayList<>(mMediaDevices), id);
-    }
-
     boolean addDeviceToPlayMedia(MediaDevice device) {
         mMetricLogger.logInteractionExpansion(device);
         return mLocalMediaManager.addDeviceToPlayMedia(device);
@@ -683,10 +672,6 @@
         return mLocalMediaManager.getDeselectableMediaDevice();
     }
 
-    void adjustSessionVolume(String sessionId, int volume) {
-        mLocalMediaManager.adjustSessionVolume(sessionId, volume);
-    }
-
     void adjustSessionVolume(int volume) {
         mLocalMediaManager.adjustSessionVolume(volume);
     }
@@ -1013,7 +998,7 @@
                 return;
             }
 
-            if (newState == PlaybackState.STATE_STOPPED || newState == PlaybackState.STATE_PAUSED) {
+            if (newState == PlaybackState.STATE_STOPPED) {
                 mCallback.onMediaStoppedOrPaused();
             }
             mCurrentState = newState;
diff --git a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
index dc1488e..53b4d43 100644
--- a/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/dream/MediaDreamSentinel.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.media.dream;
 
-import static com.android.systemui.flags.Flags.MEDIA_DREAM_COMPLICATION;
+import static com.android.systemui.flags.Flags.DREAM_MEDIA_COMPLICATION;
 
 import android.content.Context;
 import android.util.Log;
@@ -77,7 +77,7 @@
         public void onMediaDataLoaded(@NonNull String key, @Nullable String oldKey,
                 @NonNull MediaData data, boolean immediately, int receivedSmartspaceCardLatency,
                 boolean isSsReactivated) {
-            if (!mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)) {
+            if (!mFeatureFlags.isEnabled(DREAM_MEDIA_COMPLICATION)) {
                 return;
             }
 
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
new file mode 100644
index 0000000..59c6635
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.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.mediaprojection.appselector
+
+import android.content.ComponentName
+import com.android.systemui.media.dagger.MediaProjectionAppSelector
+import com.android.systemui.mediaprojection.appselector.data.RecentTask
+import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+
+class MediaProjectionAppSelectorController(
+    private val recentTaskListProvider: RecentTaskListProvider,
+    @MediaProjectionAppSelector private val scope: CoroutineScope,
+    private val appSelectorComponentName: ComponentName
+) {
+
+    fun init(view: MediaProjectionAppSelectorView) {
+        scope.launch {
+            val tasks = recentTaskListProvider.loadRecentTasks().sortTasks()
+            view.bind(tasks)
+        }
+    }
+
+    fun destroy() {
+        scope.cancel()
+    }
+
+    private fun List<RecentTask>.sortTasks(): List<RecentTask> =
+        asReversed().sortedBy {
+            // Show normal tasks first and only then tasks with opened app selector
+            it.topActivityComponent == appSelectorComponentName
+        }
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorView.kt
similarity index 60%
rename from packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
rename to packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorView.kt
index 6e17214..6550aa5 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorView.kt
@@ -14,15 +14,10 @@
  * limitations under the License.
  */
 
-package com.android.systemui.compose.gallery
+package com.android.systemui.mediaprojection.appselector
 
-import androidx.compose.foundation.layout.Column
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.android.systemui.ExampleFeature
+import com.android.systemui.mediaprojection.appselector.data.RecentTask
 
-/** The screen that shows ExampleFeature. */
-@Composable
-fun ExampleFeatureScreen(modifier: Modifier = Modifier) {
-    Column(modifier) { ExampleFeature("This is an example feature!") }
+interface MediaProjectionAppSelectorView {
+    fun bind(recentTasks: List<RecentTask>)
 }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt
new file mode 100644
index 0000000..0bdddfe
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/AppIconLoader.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.mediaprojection.appselector.data
+
+import android.content.ComponentName
+import android.graphics.drawable.Drawable
+import com.android.systemui.dagger.qualifiers.Background
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+interface AppIconLoader {
+    suspend fun loadIcon(userId: Int, component: ComponentName): Drawable?
+}
+
+class IconLoaderLibAppIconLoader
+@Inject
+constructor(
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : AppIconLoader {
+
+    override suspend fun loadIcon(userId: Int, component: ComponentName): Drawable? =
+        withContext(backgroundDispatcher) {
+            // TODO(b/240924731): add a blocking call to load an icon using iconloaderlib
+            null
+        }
+}
diff --git a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt
similarity index 60%
copy from packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
copy to packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt
index 6e17214..6d67e28 100644
--- a/packages/SystemUI/compose/gallery/src/com/android/systemui/compose/gallery/ExampleFeatureScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTask.kt
@@ -14,15 +14,13 @@
  * limitations under the License.
  */
 
-package com.android.systemui.compose.gallery
+package com.android.systemui.mediaprojection.appselector.data
 
-import androidx.compose.foundation.layout.Column
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import com.android.systemui.ExampleFeature
+import android.content.ComponentName
 
-/** The screen that shows ExampleFeature. */
-@Composable
-fun ExampleFeatureScreen(modifier: Modifier = Modifier) {
-    Column(modifier) { ExampleFeature("This is an example feature!") }
-}
+data class RecentTask(
+    val taskId: Int,
+    val userId: Int,
+    val topActivityComponent: ComponentName?,
+    val baseIntentComponent: ComponentName?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt
new file mode 100644
index 0000000..5a09435
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskListProvider.kt
@@ -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.systemui.mediaprojection.appselector.data
+
+import com.android.systemui.dagger.qualifiers.Background
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+import javax.inject.Inject
+
+interface RecentTaskListProvider {
+    suspend fun loadRecentTasks(): List<RecentTask>
+}
+
+class ShellRecentTaskListProvider
+@Inject
+constructor(@Background private val coroutineDispatcher: CoroutineDispatcher) :
+    RecentTaskListProvider {
+
+    override suspend fun loadRecentTasks(): List<RecentTask> =
+        withContext(coroutineDispatcher) {
+            // TODO(b/240924731): add blocking call to load the recents
+            emptyList()
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskThumbnailLoader.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskThumbnailLoader.kt
new file mode 100644
index 0000000..4291280
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/data/RecentTaskThumbnailLoader.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.mediaprojection.appselector.data
+
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.shared.recents.model.ThumbnailData
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+interface RecentTaskThumbnailLoader {
+    suspend fun loadThumbnail(taskId: Int): ThumbnailData?
+}
+
+class ActivityTaskManagerThumbnailLoader
+@Inject
+constructor(
+    @Background private val coroutineDispatcher: CoroutineDispatcher,
+) :
+    RecentTaskThumbnailLoader {
+
+    override suspend fun loadThumbnail(taskId: Int): ThumbnailData? =
+        withContext(coroutineDispatcher) {
+            // TODO(b/240924731): add blocking call to load a thumbnail
+             null
+        }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
new file mode 100644
index 0000000..ec5abc7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTaskViewHolder.kt
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.mediaprojection.appselector.view
+
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.recyclerview.widget.RecyclerView
+import com.android.systemui.R
+import com.android.systemui.media.dagger.MediaProjectionAppSelector
+import com.android.systemui.mediaprojection.appselector.data.AppIconLoader
+import com.android.systemui.mediaprojection.appselector.data.RecentTask
+import com.android.systemui.mediaprojection.appselector.data.RecentTaskThumbnailLoader
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+class RecentTaskViewHolder @AssistedInject constructor(
+    @Assisted root: ViewGroup,
+    private val iconLoader: AppIconLoader,
+    private val thumbnailLoader: RecentTaskThumbnailLoader,
+    @MediaProjectionAppSelector private val scope: CoroutineScope
+) : RecyclerView.ViewHolder(root) {
+
+    private val iconView: ImageView = root.requireViewById(R.id.task_icon)
+    private val thumbnailView: ImageView = root.requireViewById(R.id.task_thumbnail)
+
+    private var job: Job? = null
+
+    fun bind(task: RecentTask, onClick: (View) -> Unit) {
+        job?.cancel()
+
+        job =
+            scope.launch {
+                task.baseIntentComponent?.let { component ->
+                    launch {
+                        val icon = iconLoader.loadIcon(task.userId, component)
+                        iconView.setImageDrawable(icon)
+                    }
+                }
+                launch {
+                    val thumbnail = thumbnailLoader.loadThumbnail(task.taskId)
+                    thumbnailView.setImageBitmap(thumbnail?.thumbnail)
+                }
+            }
+
+        thumbnailView.setOnClickListener(onClick)
+    }
+
+    fun onRecycled() {
+        iconView.setImageDrawable(null)
+        thumbnailView.setImageBitmap(null)
+        job?.cancel()
+        job = null
+    }
+
+    @AssistedFactory
+    fun interface Factory {
+        fun create(root: ViewGroup): RecentTaskViewHolder
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt
new file mode 100644
index 0000000..ec9cfa8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/RecentTasksAdapter.kt
@@ -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.systemui.mediaprojection.appselector.view
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.RecyclerView
+import com.android.systemui.R
+import com.android.systemui.mediaprojection.appselector.data.RecentTask
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+class RecentTasksAdapter @AssistedInject constructor(
+    @Assisted private val items: List<RecentTask>,
+    @Assisted private val listener: RecentTaskClickListener,
+    private val viewHolderFactory: RecentTaskViewHolder.Factory
+) : RecyclerView.Adapter<RecentTaskViewHolder>() {
+
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecentTaskViewHolder {
+        val taskItem =
+            LayoutInflater.from(parent.context)
+                .inflate(R.layout.media_projection_task_item, null) as ViewGroup
+
+        return viewHolderFactory.create(taskItem)
+    }
+
+    override fun onBindViewHolder(holder: RecentTaskViewHolder, position: Int) {
+        val task = items[position]
+        holder.bind(task, onClick = {
+            listener.onRecentClicked(task, holder.itemView)
+        })
+    }
+
+    override fun getItemCount(): Int = items.size
+
+    override fun onViewRecycled(holder: RecentTaskViewHolder) {
+        holder.onRecycled()
+    }
+
+    interface RecentTaskClickListener {
+        fun onRecentClicked(task: RecentTask, view: View)
+    }
+
+    @AssistedFactory
+    fun interface Factory {
+        fun create(items: List<RecentTask>, listener: RecentTaskClickListener): RecentTasksAdapter
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java b/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java
index cd36091..1d05874 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/NonInterceptingScrollView.java
@@ -28,21 +28,29 @@
 public class NonInterceptingScrollView extends ScrollView {
 
     private final int mTouchSlop;
+
     private float mDownY;
     private boolean mScrollEnabled = true;
+    private boolean mPreventingIntercept;
 
     public NonInterceptingScrollView(Context context, AttributeSet attrs) {
         super(context, attrs);
         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
     }
 
+    public boolean isPreventingIntercept() {
+        return mPreventingIntercept;
+    }
+
     @Override
     public boolean onTouchEvent(MotionEvent ev) {
         int action = ev.getActionMasked();
         switch (action) {
             case MotionEvent.ACTION_DOWN:
+                mPreventingIntercept = false;
                 if (canScrollVertically(1)) {
                     // If we can scroll down, make sure we're not intercepted by the parent
+                    mPreventingIntercept = true;
                     final ViewParent parent = getParent();
                     if (parent != null) {
                         parent.requestDisallowInterceptTouchEvent(true);
@@ -62,10 +70,13 @@
     public boolean onInterceptTouchEvent(MotionEvent ev) {
         // If there's a touch on this view and we can scroll down, we don't want to be intercepted
         int action = ev.getActionMasked();
+
         switch (action) {
             case MotionEvent.ACTION_DOWN:
-                // If we can scroll down, make sure non of our parents intercepts us.
+                mPreventingIntercept = false;
+                // If we can scroll down, make sure none of our parents intercepts us.
                 if (canScrollVertically(1)) {
+                    mPreventingIntercept = true;
                     final ViewParent parent = getParent();
                     if (parent != null) {
                         parent.requestDisallowInterceptTouchEvent(true);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
index 7b1ddd6..ef87fb4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
@@ -131,6 +131,10 @@
         updateClippingPath();
     }
 
+    public NonInterceptingScrollView getQSPanelContainer() {
+        return mQSPanelContainer;
+    }
+
     public void disable(int state1, int state2, boolean animate) {
         final boolean disabled = (state2 & DISABLE2_QUICK_SETTINGS) != 0;
         if (disabled == mQsDisabled) return;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
index 7d61991..dea7bb5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java
@@ -16,8 +16,13 @@
 
 package com.android.systemui.qs;
 
-import android.content.res.Configuration;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
 
+import android.content.res.Configuration;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.statusbar.policy.ConfigurationController;
 import com.android.systemui.util.ViewController;
@@ -30,6 +35,8 @@
     private final QSPanelController mQsPanelController;
     private final QuickStatusBarHeaderController mQuickStatusBarHeaderController;
     private final ConfigurationController mConfigurationController;
+    private final FalsingManager mFalsingManager;
+    private final NonInterceptingScrollView mQSPanelContainer;
 
     private final ConfigurationController.ConfigurationListener mConfigurationListener =
             new ConfigurationController.ConfigurationListener() {
@@ -39,14 +46,32 @@
         }
     };
 
+    private final View.OnTouchListener mContainerTouchHandler = new View.OnTouchListener() {
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                if (mQSPanelContainer.isPreventingIntercept()) {
+                    // There's really no action here to take, but we need to tell the FalsingManager
+                    mFalsingManager.isFalseTouch(QS_SWIPE_NESTED);
+                }
+            }
+            return false;
+        }
+    };
+
     @Inject
-    QSContainerImplController(QSContainerImpl view, QSPanelController qsPanelController,
+    QSContainerImplController(
+            QSContainerImpl view,
+            QSPanelController qsPanelController,
             QuickStatusBarHeaderController quickStatusBarHeaderController,
-            ConfigurationController configurationController) {
+            ConfigurationController configurationController,
+            FalsingManager falsingManager) {
         super(view);
         mQsPanelController = qsPanelController;
         mQuickStatusBarHeaderController = quickStatusBarHeaderController;
         mConfigurationController = configurationController;
+        mFalsingManager = falsingManager;
+        mQSPanelContainer = mView.getQSPanelContainer();
     }
 
     @Override
@@ -62,11 +87,13 @@
     protected void onViewAttached() {
         mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController);
         mConfigurationController.addCallback(mConfigurationListener);
+        mQSPanelContainer.setOnTouchListener(mContainerTouchHandler);
     }
 
     @Override
     protected void onViewDetached() {
         mConfigurationController.removeCallback(mConfigurationListener);
+        mQSPanelContainer.setOnTouchListener(null);
     }
 
     public QSContainerImpl getView() {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 448e180..184089f7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -473,6 +473,8 @@
                     ? Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0) : 0;
             layoutParams.topMargin = mediaNeedsTopMargin() && !horizontal
                     ? mMediaTopMargin : 0;
+            // Call setLayoutParams explicitly to ensure that requestLayout happens
+            hostView.setLayoutParams(layoutParams);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
index 59b871c..f41b905 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java
@@ -16,12 +16,11 @@
 
 package com.android.systemui.qs;
 
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.media.dagger.MediaModule.QS_PANEL;
 import static com.android.systemui.qs.QSPanel.QS_SHOW_BRIGHTNESS;
 import static com.android.systemui.qs.dagger.QSFragmentModule.QS_USING_MEDIA_PLAYER;
 
-import android.content.res.Configuration;
 import android.view.MotionEvent;
 import android.view.View;
 
@@ -61,22 +60,11 @@
     private final BrightnessMirrorHandler mBrightnessMirrorHandler;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
 
-    private final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
-            new QSPanel.OnConfigurationChangedListener() {
-        @Override
-        public void onConfigurationChange(Configuration newConfig) {
-            mView.updateResources();
-            if (mView.isListening()) {
-                refreshAllTiles();
-            }
-        }
-    };
-
     private View.OnTouchListener mTileLayoutTouchListener = new View.OnTouchListener() {
         @Override
         public boolean onTouch(View v, MotionEvent event) {
             if (event.getActionMasked() == MotionEvent.ACTION_UP) {
-                mFalsingManager.isFalseTouch(QS_SWIPE);
+                mFalsingManager.isFalseTouch(QS_SWIPE_SIDE);
             }
             return false;
         }
@@ -130,7 +118,6 @@
         if (mView.isListening()) {
             refreshAllTiles();
         }
-        mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
         switchTileLayout(true);
         mBrightnessMirrorHandler.onQsPanelAttached();
 
@@ -147,11 +134,18 @@
     @Override
     protected void onViewDetached() {
         mTunerService.removeTunable(mView);
-        mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener);
         mBrightnessMirrorHandler.onQsPanelDettached();
         super.onViewDetached();
     }
 
+    @Override
+    protected void onConfigurationChanged() {
+        mView.updateResources();
+        if (mView.isListening()) {
+            refreshAllTiles();
+        }
+    }
+
     /** */
     public void setVisibility(int visibility) {
         mView.setVisibility(visibility);
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 6e4c858..a5c60a4 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -101,11 +101,11 @@
                                     + newConfig.windowConfiguration);
                     mQSLogger.logOnConfigurationChanged(mLastOrientation, newConfig.orientation,
                             mView.getDumpableTag());
-                    onConfigurationChanged();
                     if (newConfig.orientation != mLastOrientation) {
                         mLastOrientation = newConfig.orientation;
                         switchTileLayout(false);
                     }
+                    onConfigurationChanged();
                 }
             };
 
@@ -422,6 +422,8 @@
         }
         if (mMediaHost != null) {
             pw.println("  media bounds: " + mMediaHost.getCurrentBounds());
+            pw.println("  horizontal layout: " + mUsingHorizontalLayout);
+            pw.println("  last orientation: " + mLastOrientation);
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
index c86e6e8..7ce0ad0 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanelController.java
@@ -46,14 +46,6 @@
 @QSScope
 public class QuickQSPanelController extends QSPanelControllerBase<QuickQSPanel> {
 
-    private final QSPanel.OnConfigurationChangedListener mOnConfigurationChangedListener =
-            newConfig -> {
-                int newMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_tiles);
-                if (newMaxTiles != mView.getNumQuickTiles()) {
-                    setMaxTiles(newMaxTiles);
-                }
-            };
-
     private final Provider<Boolean> mUsingCollapsedLandscapeMediaProvider;
 
     @Inject
@@ -99,13 +91,11 @@
     @Override
     protected void onViewAttached() {
         super.onViewAttached();
-        mView.addOnConfigurationChangedListener(mOnConfigurationChangedListener);
     }
 
     @Override
     protected void onViewDetached() {
         super.onViewDetached();
-        mView.removeOnConfigurationChangedListener(mOnConfigurationChangedListener);
     }
 
     private void setMaxTiles(int parseNumTiles) {
@@ -115,6 +105,10 @@
 
     @Override
     protected void onConfigurationChanged() {
+        int newMaxTiles = getResources().getInteger(R.integer.quick_qs_panel_max_tiles);
+        if (newMaxTiles != mView.getNumQuickTiles()) {
+            setMaxTiles(newMaxTiles);
+        }
         updateMediaExpansion();
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
index 0ec4eef..97476b2 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java
@@ -16,9 +16,6 @@
 
 package com.android.systemui.qs.tiles;
 
-import static com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA;
-import static com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA;
-
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.drawable.Drawable;
@@ -42,6 +39,7 @@
 import com.android.systemui.qs.QSUserSwitcherEvent;
 import com.android.systemui.qs.user.UserSwitchDialogController;
 import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.user.data.source.UserRecord;
 
@@ -73,7 +71,8 @@
         mAdapter.refresh();
     }
 
-    public static class Adapter extends UserSwitcherController.BaseUserAdapter
+    /** Provides views for user detail items. */
+    public static class Adapter extends BaseUserSwitcherAdapter
             implements OnClickListener {
 
         private final Context mContext;
@@ -137,7 +136,7 @@
             v.setActivated(item.isCurrent);
             v.setDisabledByAdmin(mController.isDisabledByAdmin(item));
             v.setEnabled(item.isSwitchToEnabled);
-            v.setAlpha(v.isEnabled() ? USER_SWITCH_ENABLED_ALPHA : USER_SWITCH_DISABLED_ALPHA);
+            UserSwitcherController.setSelectableAlpha(v);
 
             if (item.isCurrent) {
                 mCurrentUserView = v;
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 8d74a09..6be9bbb 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -31,10 +31,15 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.keyguard.AuthKeyguardMessageArea;
 import com.android.keyguard.LockIconViewController;
+import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.R;
 import com.android.systemui.classifier.FalsingCollector;
 import com.android.systemui.dock.DockManager;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.ui.binder.KeyguardBouncerViewBinder;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -108,7 +113,10 @@
             NotificationShadeWindowController controller,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
             AmbientState ambientState,
-            PulsingGestureListener pulsingGestureListener
+            PulsingGestureListener pulsingGestureListener,
+            FeatureFlags featureFlags,
+            KeyguardBouncerViewModel keyguardBouncerViewModel,
+            KeyguardBouncerComponent.Factory keyguardBouncerComponentFactory
     ) {
         mLockscreenShadeTransitionController = transitionController;
         mFalsingCollector = falsingCollector;
@@ -130,6 +138,12 @@
 
         // This view is not part of the newly inflated expanded status bar.
         mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
+        if (featureFlags.isEnabled(Flags.MODERN_BOUNCER)) {
+            KeyguardBouncerViewBinder.bind(
+                    mView.findViewById(R.id.keyguard_bouncer_container),
+                    keyguardBouncerViewModel,
+                    keyguardBouncerComponentFactory);
+        }
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index e06c977..073ab8b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -1199,7 +1199,8 @@
             return ((!updateMonitor.isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)
                     && msgId != FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT)
                     || msgId == FingerprintManager.FINGERPRINT_ERROR_CANCELED
-                    || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED);
+                    || msgId == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED
+                    || msgId == FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED);
         }
 
         private boolean shouldSuppressFaceError(int msgId, KeyguardUpdateMonitor updateMonitor) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/UserUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/UserUtil.java
deleted file mode 100644
index 4551807..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/UserUtil.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * 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.statusbar;
-
-import android.content.Context;
-import android.content.DialogInterface;
-
-import com.android.systemui.R;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
-import com.android.systemui.statusbar.policy.UserSwitcherController;
-
-public class UserUtil {
-    public static void deleteUserWithPrompt(Context context, int userId,
-                                            UserSwitcherController userSwitcherController) {
-        new RemoveUserDialog(context, userId, userSwitcherController).show();
-    }
-
-    private final static class RemoveUserDialog extends SystemUIDialog implements
-            DialogInterface.OnClickListener {
-
-        private final int mUserId;
-        private final UserSwitcherController mUserSwitcherController;
-
-        public RemoveUserDialog(Context context, int userId,
-                                UserSwitcherController userSwitcherController) {
-            super(context);
-            setTitle(R.string.user_remove_user_title);
-            setMessage(context.getString(R.string.user_remove_user_message));
-            setButton(DialogInterface.BUTTON_NEUTRAL,
-                    context.getString(android.R.string.cancel), this);
-            setButton(DialogInterface.BUTTON_POSITIVE,
-                    context.getString(R.string.user_remove_user_remove), this);
-            setCanceledOnTouchOutside(false);
-            mUserId = userId;
-            mUserSwitcherController = userSwitcherController;
-        }
-
-        @Override
-        public void onClick(DialogInterface dialog, int which) {
-            if (which == BUTTON_NEUTRAL) {
-                cancel();
-            } else {
-                dismiss();
-                mUserSwitcherController.removeUserId(mUserId);
-            }
-        }
-    }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index d380e9f..d61c51e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -56,7 +56,9 @@
 
 /**
  * A class which manages the bouncer on the lockscreen.
+ * @deprecated Use KeyguardBouncerRepository
  */
+@Deprecated
 public class KeyguardBouncer {
 
     private static final String TAG = "KeyguardBouncer";
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
index 4d61689..00c3e8f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/MultiUserSwitchController.java
@@ -33,6 +33,7 @@
 import com.android.systemui.qs.FooterActionsView;
 import com.android.systemui.qs.dagger.QSScope;
 import com.android.systemui.qs.user.UserSwitchDialogController;
+import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter;
 import com.android.systemui.statusbar.policy.UserSwitcherController;
 import com.android.systemui.user.UserSwitcherActivity;
 import com.android.systemui.util.ViewController;
@@ -49,7 +50,7 @@
     private final ActivityStarter mActivityStarter;
     private final FeatureFlags mFeatureFlags;
 
-    private UserSwitcherController.BaseUserAdapter mUserListener;
+    private BaseUserSwitcherAdapter mUserListener;
 
     private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
         @Override
@@ -135,7 +136,7 @@
 
             final UserSwitcherController controller = mUserSwitcherController;
             if (controller != null) {
-                mUserListener = new UserSwitcherController.BaseUserAdapter(controller) {
+                mUserListener = new BaseUserSwitcherAdapter(controller) {
                     @Override
                     public void notifyDataSetChanged() {
                         mView.refreshContentDescription(getCurrentUser());
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 7867147..e61794b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -46,6 +46,7 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.AuthKeyguardMessageArea;
 import com.android.keyguard.KeyguardMessageAreaController;
+import com.android.keyguard.KeyguardSecurityModel;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.KeyguardUpdateMonitorCallback;
 import com.android.keyguard.KeyguardViewController;
@@ -53,6 +54,12 @@
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.data.BouncerView;
+import com.android.systemui.keyguard.data.BouncerViewDelegate;
+import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor;
 import com.android.systemui.navigationbar.NavigationBarView;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -123,6 +130,9 @@
     @Nullable
     private final FoldAodAnimationController mFoldAodAnimationController;
     private KeyguardMessageAreaController<AuthKeyguardMessageArea> mKeyguardMessageAreaController;
+    private final BouncerCallbackInteractor mBouncerCallbackInteractor;
+    private final BouncerInteractor mBouncerInteractor;
+    private final BouncerViewDelegate mBouncerViewDelegate;
     private final Lazy<com.android.systemui.shade.ShadeController> mShadeController;
 
     private final BouncerExpansionCallback mExpansionCallback = new BouncerExpansionCallback() {
@@ -197,7 +207,7 @@
 
     private View mNotificationContainer;
 
-    protected KeyguardBouncer mBouncer;
+    @Nullable protected KeyguardBouncer mBouncer;
     protected boolean mShowing;
     protected boolean mOccluded;
     protected boolean mRemoteInputActive;
@@ -223,6 +233,7 @@
     private int mLastBiometricMode;
     private boolean mLastScreenOffAnimationPlaying;
     private float mQsExpansion;
+    private boolean mIsModernBouncerEnabled;
 
     private OnDismissAction mAfterKeyguardGoneAction;
     private Runnable mKeyguardGoneCancelAction;
@@ -237,6 +248,7 @@
     private final DockManager mDockManager;
     private final KeyguardUpdateMonitor mKeyguardUpdateManager;
     private final LatencyTracker mLatencyTracker;
+    private final KeyguardSecurityModel mKeyguardSecurityModel;
     private KeyguardBypassController mBypassController;
     @Nullable private AlternateAuthInterceptor mAlternateAuthInterceptor;
 
@@ -271,7 +283,12 @@
             KeyguardMessageAreaController.Factory keyguardMessageAreaFactory,
             Optional<SysUIUnfoldComponent> sysUIUnfoldComponent,
             Lazy<ShadeController> shadeController,
-            LatencyTracker latencyTracker) {
+            LatencyTracker latencyTracker,
+            KeyguardSecurityModel keyguardSecurityModel,
+            FeatureFlags featureFlags,
+            BouncerCallbackInteractor bouncerCallbackInteractor,
+            BouncerInteractor bouncerInteractor,
+            BouncerView bouncerView) {
         mContext = context;
         mViewMediatorCallback = callback;
         mLockPatternUtils = lockPatternUtils;
@@ -288,8 +305,13 @@
         mKeyguardMessageAreaFactory = keyguardMessageAreaFactory;
         mShadeController = shadeController;
         mLatencyTracker = latencyTracker;
+        mKeyguardSecurityModel = keyguardSecurityModel;
+        mBouncerCallbackInteractor = bouncerCallbackInteractor;
+        mBouncerInteractor = bouncerInteractor;
+        mBouncerViewDelegate = bouncerView.getDelegate();
         mFoldAodAnimationController = sysUIUnfoldComponent
                 .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null);
+        mIsModernBouncerEnabled = featureFlags.isEnabled(Flags.MODERN_BOUNCER);
     }
 
     @Override
@@ -303,7 +325,11 @@
         mBiometricUnlockController = biometricUnlockController;
 
         ViewGroup container = mCentralSurfaces.getBouncerContainer();
-        mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
+        if (mIsModernBouncerEnabled) {
+            mBouncerCallbackInteractor.addBouncerExpansionCallback(mExpansionCallback);
+        } else {
+            mBouncer = mKeyguardBouncerFactory.create(container, mExpansionCallback);
+        }
         mNotificationPanelViewController = notificationPanelViewController;
         if (panelExpansionStateManager != null) {
             panelExpansionStateManager.addExpansionListener(this);
@@ -377,29 +403,45 @@
         if (mDozing && !mPulsing) {
             return;
         } else if (mNotificationPanelViewController.isUnlockHintRunning()) {
-            mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+            if (mBouncer != null) {
+                mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+            }
+            mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
         } else if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
             // Don't expand to the bouncer. Instead transition back to the lock screen (see
             // CentralSurfaces#showBouncerOrLockScreenIfKeyguard)
             return;
         } else if (bouncerNeedsScrimming()) {
-            mBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
+            if (mBouncer != null) {
+                mBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
+            }
+            mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
         } else if (mShowing && !hideBouncerOverDream) {
             if (!isWakeAndUnlocking()
                     && !(mBiometricUnlockController.getMode() == MODE_DISMISS_BOUNCER)
                     && !mCentralSurfaces.isInLaunchTransition()
                     && !isUnlockCollapsing()) {
-                mBouncer.setExpansion(fraction);
+                if (mBouncer != null) {
+                    mBouncer.setExpansion(fraction);
+                }
+                mBouncerInteractor.setExpansion(fraction);
             }
             if (fraction != KeyguardBouncer.EXPANSION_HIDDEN && tracking
                     && !mKeyguardStateController.canDismissLockScreen()
-                    && !mBouncer.isShowing() && !mBouncer.isAnimatingAway()) {
-                mBouncer.show(false /* resetSecuritySelection */, false /* scrimmed */);
+                    && !bouncerIsShowing()
+                    && !bouncerIsAnimatingAway()) {
+                if (mBouncer != null) {
+                    mBouncer.show(false /* resetSecuritySelection */, false /* scrimmed */);
+                }
+                mBouncerInteractor.show(/* isScrimmed= */false);
             }
-        } else if (!mShowing && mBouncer.inTransit()) {
+        } else if (!mShowing && isBouncerInTransit()) {
             // Keyguard is not visible anymore, but expansion animation was still running.
             // We need to hide the bouncer, otherwise it will be stuck in transit.
-            mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+            if (mBouncer != null) {
+                mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+            }
+            mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
         } else if (mPulsing && fraction == KeyguardBouncer.EXPANSION_VISIBLE) {
             // Panel expanded while pulsing but didn't translate the bouncer (because we are
             // unlocked.) Let's simply wake-up to dismiss the lock screen.
@@ -440,15 +482,20 @@
      * {@link KeyguardBouncer#needsFullscreenBouncer()}.
      */
     protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
-        if (mBouncer.needsFullscreenBouncer() && !mDozing) {
+        if (needsFullscreenBouncer() && !mDozing) {
             // The keyguard might be showing (already). So we need to hide it.
             mCentralSurfaces.hideKeyguard();
-            mBouncer.show(true /* resetSecuritySelection */);
+            if (mBouncer != null) {
+                mBouncer.show(true /* resetSecuritySelection */);
+            }
+            mBouncerInteractor.show(true);
         } else {
             mCentralSurfaces.showKeyguard();
             if (hideBouncerWhenShowing) {
                 hideBouncer(false /* destroyView */);
-                mBouncer.prepare();
+                if (mBouncer != null) {
+                    mBouncer.prepare();
+                }
             }
         }
         updateStates();
@@ -480,10 +527,10 @@
      */
     @VisibleForTesting
     void hideBouncer(boolean destroyView) {
-        if (mBouncer == null) {
-            return;
+        if (mBouncer != null) {
+            mBouncer.hide(destroyView);
         }
-        mBouncer.hide(destroyView);
+        mBouncerInteractor.hide();
         if (mShowing) {
             // If we were showing the bouncer and then aborting, we need to also clear out any
             // potential actions unless we actually unlocked.
@@ -501,8 +548,11 @@
     public void showBouncer(boolean scrimmed) {
         resetAlternateAuth(false);
 
-        if (mShowing && !mBouncer.isShowing()) {
-            mBouncer.show(false /* resetSecuritySelection */, scrimmed);
+        if (mShowing && !isBouncerShowing()) {
+            if (mBouncer != null) {
+                mBouncer.show(false /* resetSecuritySelection */, scrimmed);
+            }
+            mBouncerInteractor.show(scrimmed);
         }
         updateStates();
     }
@@ -535,7 +585,11 @@
                 // instead of the bouncer.
                 if (shouldShowAltAuth()) {
                     if (!afterKeyguardGone) {
-                        mBouncer.setDismissAction(mAfterKeyguardGoneAction,
+                        if (mBouncer != null) {
+                            mBouncer.setDismissAction(mAfterKeyguardGoneAction,
+                                    mKeyguardGoneCancelAction);
+                        }
+                        mBouncerInteractor.setDismissAction(mAfterKeyguardGoneAction,
                                 mKeyguardGoneCancelAction);
                         mAfterKeyguardGoneAction = null;
                         mKeyguardGoneCancelAction = null;
@@ -549,12 +603,18 @@
                 if (afterKeyguardGone) {
                     // we'll handle the dismiss action after keyguard is gone, so just show the
                     // bouncer
-                    mBouncer.show(false /* resetSecuritySelection */);
+                    mBouncerInteractor.show(/* isScrimmed= */true);
+                    if (mBouncer != null) mBouncer.show(false /* resetSecuritySelection */);
                 } else {
                     // after authentication success, run dismiss action with the option to defer
                     // hiding the keyguard based on the return value of the OnDismissAction
-                    mBouncer.showWithDismissAction(mAfterKeyguardGoneAction,
-                            mKeyguardGoneCancelAction);
+                    mBouncerInteractor.setDismissAction(
+                            mAfterKeyguardGoneAction, mKeyguardGoneCancelAction);
+                    mBouncerInteractor.show(/* isScrimmed= */true);
+                    if (mBouncer != null) {
+                        mBouncer.showWithDismissAction(mAfterKeyguardGoneAction,
+                                mKeyguardGoneCancelAction);
+                    }
                     // bouncer will handle the dismiss action, so we no longer need to track it here
                     mAfterKeyguardGoneAction = null;
                     mKeyguardGoneCancelAction = null;
@@ -591,7 +651,7 @@
             // Hide bouncer and quick-quick settings.
             if (mOccluded && !mDozing) {
                 mCentralSurfaces.hideKeyguard();
-                if (hideBouncerWhenShowing || mBouncer.needsFullscreenBouncer()) {
+                if (hideBouncerWhenShowing || needsFullscreenBouncer()) {
                     hideBouncer(false /* destroyView */);
                 }
             } else {
@@ -655,7 +715,10 @@
 
     @Override
     public void onFinishedGoingToSleep() {
-        mBouncer.onScreenTurnedOff();
+        if (mBouncer != null) {
+            mBouncer.onScreenTurnedOff();
+        }
+        mBouncerInteractor.onScreenTurnedOff();
     }
 
     @Override
@@ -746,7 +809,7 @@
             // by a FLAG_DISMISS_KEYGUARD_ACTIVITY.
             reset(isOccluding /* hideBouncerWhenShowing*/);
         }
-        if (animate && !mOccluded && mShowing && !mBouncer.isShowing()) {
+        if (animate && !mOccluded && mShowing && !bouncerIsShowing()) {
             mCentralSurfaces.animateKeyguardUnoccluding();
         }
     }
@@ -762,8 +825,11 @@
 
     @Override
     public void startPreHideAnimation(Runnable finishRunnable) {
-        if (mBouncer.isShowing()) {
-            mBouncer.startPreHideAnimation(finishRunnable);
+        if (bouncerIsShowing()) {
+            if (mBouncer != null) {
+                mBouncer.startPreHideAnimation(finishRunnable);
+            }
+            mBouncerInteractor.startDisappearAnimation(finishRunnable);
             mCentralSurfaces.onBouncerPreHideAnimation();
 
             // We update the state (which will show the keyguard) only if an animation will run on
@@ -873,8 +939,12 @@
     }
 
     public void onThemeChanged() {
-        boolean wasShowing = mBouncer.isShowing();
-        boolean wasScrimmed = mBouncer.isScrimmed();
+        if (mIsModernBouncerEnabled) {
+            updateResources();
+            return;
+        }
+        boolean wasShowing = bouncerIsShowing();
+        boolean wasScrimmed = bouncerIsScrimmed();
 
         hideBouncer(true /* destroyView */);
         mBouncer.prepare();
@@ -924,7 +994,12 @@
      * WARNING: This method might cause Binder calls.
      */
     public boolean isSecure() {
-        return mBouncer.isSecure();
+        if (mBouncer != null) {
+            return mBouncer.isSecure();
+        }
+
+        return mKeyguardSecurityModel.getSecurityMode(
+                KeyguardUpdateMonitor.getCurrentUser()) != KeyguardSecurityModel.SecurityMode.None;
     }
 
     @Override
@@ -941,10 +1016,11 @@
      * @return whether the back press has been handled
      */
     public boolean onBackPressed(boolean hideImmediately) {
-        if (mBouncer.isShowing()) {
+        if (bouncerIsShowing()) {
             mCentralSurfaces.endAffordanceLaunch();
             // The second condition is for SIM card locked bouncer
-            if (mBouncer.isScrimmed() && !mBouncer.needsFullscreenBouncer()) {
+            if (bouncerIsScrimmed()
+                    && !needsFullscreenBouncer()) {
                 hideBouncer(false);
                 updateStates();
             } else {
@@ -957,16 +1033,19 @@
 
     @Override
     public boolean isBouncerShowing() {
-        return mBouncer.isShowing() || isShowingAlternateAuth();
+        return bouncerIsShowing() || isShowingAlternateAuth();
     }
 
     @Override
     public boolean bouncerIsOrWillBeShowing() {
-        return isBouncerShowing() || mBouncer.inTransit();
+        return isBouncerShowing() || isBouncerInTransit();
     }
 
     public boolean isFullscreenBouncer() {
-        return mBouncer.isFullscreenBouncer();
+        if (mBouncerViewDelegate != null) {
+            return mBouncerViewDelegate.isFullScreenBouncer();
+        }
+        return mBouncer != null && mBouncer.isFullscreenBouncer();
     }
 
     /**
@@ -987,7 +1066,7 @@
     private long getNavBarShowDelay() {
         if (mKeyguardStateController.isKeyguardFadingAway()) {
             return mKeyguardStateController.getKeyguardFadingAwayDelay();
-        } else if (mBouncer.isShowing()) {
+        } else if (isBouncerShowing()) {
             return NAV_BAR_SHOW_DELAY_BOUNCER;
         } else {
             // No longer dozing, or remote input is active. No delay.
@@ -1010,18 +1089,24 @@
     protected void updateStates() {
         boolean showing = mShowing;
         boolean occluded = mOccluded;
-        boolean bouncerShowing = mBouncer.isShowing();
+        boolean bouncerShowing = bouncerIsShowing();
         boolean bouncerIsOrWillBeShowing = bouncerIsOrWillBeShowing();
-        boolean bouncerDismissible = !mBouncer.isFullscreenBouncer();
+        boolean bouncerDismissible = !isFullscreenBouncer();
         boolean remoteInputActive = mRemoteInputActive;
 
         if ((bouncerDismissible || !showing || remoteInputActive) !=
                 (mLastBouncerDismissible || !mLastShowing || mLastRemoteInputActive)
                 || mFirstUpdate) {
             if (bouncerDismissible || !showing || remoteInputActive) {
-                mBouncer.setBackButtonEnabled(true);
+                if (mBouncer != null) {
+                    mBouncer.setBackButtonEnabled(true);
+                }
+                mBouncerInteractor.setBackButtonEnabled(true);
             } else {
-                mBouncer.setBackButtonEnabled(false);
+                if (mBouncer != null) {
+                    mBouncer.setBackButtonEnabled(false);
+                }
+                mBouncerInteractor.setBackButtonEnabled(false);
             }
         }
 
@@ -1098,7 +1183,9 @@
                 || mPulsing && !mIsDocked)
                 && mGesturalNav;
         return (!keyguardShowing && !hideWhileDozing && !mScreenOffAnimationPlaying
-                || mBouncer.isShowing() || mRemoteInputActive || keyguardWithGestureNav
+                || bouncerIsShowing()
+                || mRemoteInputActive
+                || keyguardWithGestureNav
                 || mGlobalActionsVisible);
     }
 
@@ -1117,18 +1204,27 @@
     }
 
     public boolean shouldDismissOnMenuPressed() {
-        return mBouncer.shouldDismissOnMenuPressed();
+        if (mBouncerViewDelegate != null) {
+            return mBouncerViewDelegate.shouldDismissOnMenuPressed();
+        }
+        return mBouncer != null && mBouncer.shouldDismissOnMenuPressed();
     }
 
     public boolean interceptMediaKey(KeyEvent event) {
-        return mBouncer.interceptMediaKey(event);
+        if (mBouncerViewDelegate != null) {
+            return mBouncerViewDelegate.interceptMediaKey(event);
+        }
+        return mBouncer != null && mBouncer.interceptMediaKey(event);
     }
 
     /**
      * @return true if the pre IME back event should be handled
      */
     public boolean dispatchBackKeyEventPreIme() {
-        return mBouncer.dispatchBackKeyEventPreIme();
+        if (mBouncerViewDelegate != null) {
+            return mBouncerViewDelegate.dispatchBackKeyEventPreIme();
+        }
+        return mBouncer != null && mBouncer.dispatchBackKeyEventPreIme();
     }
 
     public void readyForKeyguardDone() {
@@ -1151,7 +1247,7 @@
     }
 
     public boolean isSecure(int userId) {
-        return mBouncer.isSecure() || mLockPatternUtils.isSecure(userId);
+        return isSecure() || mLockPatternUtils.isSecure(userId);
     }
 
     @Override
@@ -1174,7 +1270,10 @@
      * fingerprint.
      */
     public void notifyKeyguardAuthenticated(boolean strongAuth) {
-        mBouncer.notifyKeyguardAuthenticated(strongAuth);
+        if (mBouncer != null) {
+            mBouncer.notifyKeyguardAuthenticated(strongAuth);
+        }
+        mBouncerInteractor.notifyKeyguardAuthenticated(strongAuth);
 
         if (mAlternateAuthInterceptor != null && isShowingAlternateAuthOrAnimating()) {
             resetAlternateAuth(false);
@@ -1189,7 +1288,10 @@
                 mKeyguardMessageAreaController.setMessage(message);
             }
         } else {
-            mBouncer.showMessage(message, colorState);
+            if (mBouncer != null) {
+                mBouncer.showMessage(message, colorState);
+            }
+            mBouncerInteractor.showMessage(message, colorState);
         }
     }
 
@@ -1222,9 +1324,10 @@
     public boolean bouncerNeedsScrimming() {
         // When a dream overlay is active, scrimming will cause any expansion to immediately expand.
         return (mOccluded && !mDreamOverlayStateController.isOverlayActive())
-                || mBouncer.willDismissWithAction()
-                || (mBouncer.isShowing() && mBouncer.isScrimmed())
-                || mBouncer.isFullscreenBouncer();
+                || bouncerWillDismissWithAction()
+                || (bouncerIsShowing()
+                && bouncerIsScrimmed())
+                || isFullscreenBouncer();
     }
 
     /**
@@ -1236,6 +1339,7 @@
         if (mBouncer != null) {
             mBouncer.updateResources();
         }
+        mBouncerInteractor.updateResources();
     }
 
     public void dump(PrintWriter pw) {
@@ -1289,6 +1393,7 @@
         }
     }
 
+    @Nullable
     public KeyguardBouncer getBouncer() {
         return mBouncer;
     }
@@ -1320,6 +1425,8 @@
         if (mBouncer != null) {
             mBouncer.updateKeyguardPosition(x);
         }
+
+        mBouncerInteractor.setKeyguardPosition(x);
     }
 
     private static class DismissWithActionRequest {
@@ -1359,9 +1466,65 @@
      * Returns if bouncer expansion is between 0 and 1 non-inclusive.
      */
     public boolean isBouncerInTransit() {
-        if (mBouncer == null) return false;
+        if (mBouncer != null) {
+            return mBouncer.inTransit();
+        }
 
-        return mBouncer.inTransit();
+        return mBouncerInteractor.isInTransit();
+    }
+
+    /**
+     * Returns if bouncer is showing
+     */
+    public boolean bouncerIsShowing() {
+        if (mBouncer != null) {
+            return mBouncer.isShowing();
+        }
+
+        return mBouncerInteractor.isFullyShowing();
+    }
+
+    /**
+     * Returns if bouncer is scrimmed
+     */
+    public boolean bouncerIsScrimmed() {
+        if (mBouncer != null) {
+            return mBouncer.isScrimmed();
+        }
+
+        return mBouncerInteractor.isScrimmed();
+    }
+
+    /**
+     * Returns if bouncer is animating away
+     */
+    public boolean bouncerIsAnimatingAway() {
+        if (mBouncer != null) {
+            return mBouncer.isAnimatingAway();
+        }
+
+        return mBouncerInteractor.isAnimatingAway();
+    }
+
+    /**
+     * Returns if bouncer will dismiss with action
+     */
+    public boolean bouncerWillDismissWithAction() {
+        if (mBouncer != null) {
+            return mBouncer.willDismissWithAction();
+        }
+
+        return mBouncerInteractor.willDismissWithAction();
+    }
+
+    /**
+     * Returns if bouncer needs fullscreen bouncer. i.e. sim pin security method
+     */
+    public boolean needsFullscreenBouncer() {
+        KeyguardSecurityModel.SecurityMode mode = mKeyguardSecurityModel.getSecurityMode(
+                KeyguardUpdateMonitor.getCurrentUser());
+        return mode == KeyguardSecurityModel.SecurityMode.SimPin
+                || mode == KeyguardSecurityModel.SecurityMode.SimPuk;
     }
 
     /**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
new file mode 100644
index 0000000..5b2d695
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapter.kt
@@ -0,0 +1,119 @@
+/*
+ * 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.policy
+
+import android.content.Context
+import android.graphics.ColorFilter
+import android.graphics.ColorMatrix
+import android.graphics.ColorMatrixColorFilter
+import android.graphics.drawable.Drawable
+import android.os.UserHandle
+import android.widget.BaseAdapter
+import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper.getUserRecordName
+import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper.getUserSwitcherActionIconResourceId
+import java.lang.ref.WeakReference
+
+/** Provides views for user switcher experiences. */
+abstract class BaseUserSwitcherAdapter
+protected constructor(
+    protected val controller: UserSwitcherController,
+) : BaseAdapter() {
+
+    protected open val users: ArrayList<UserRecord>
+        get() = controller.users
+
+    init {
+        controller.addAdapter(WeakReference(this))
+    }
+
+    override fun getCount(): Int {
+        return if (controller.isKeyguardShowing) {
+            users.count { !it.isRestricted }
+        } else {
+            users.size
+        }
+    }
+
+    override fun getItem(position: Int): UserRecord {
+        return users[position]
+    }
+
+    override fun getItemId(position: Int): Long {
+        return position.toLong()
+    }
+
+    /**
+     * Notifies that a user item in the UI has been clicked.
+     *
+     * If the user switcher is hosted in a dialog, passing a non-null [dialogShower] will allow
+     * animation to and from the parent dialog.
+     */
+    @JvmOverloads
+    fun onUserListItemClicked(
+        record: UserRecord,
+        dialogShower: DialogShower? = null,
+    ) {
+        controller.onUserListItemClicked(record, dialogShower)
+    }
+
+    open fun getName(context: Context, item: UserRecord): String {
+        return getName(context, item, false)
+    }
+
+    /** Returns the name for the given {@link UserRecord}. */
+    open fun getName(context: Context, item: UserRecord, isTablet: Boolean): String {
+        return getUserRecordName(
+            context = context,
+            record = item,
+            isGuestUserAutoCreated = controller.isGuestUserAutoCreated,
+            isGuestUserResetting = controller.isGuestUserResetting,
+            isTablet = isTablet,
+        )
+    }
+
+    fun refresh() {
+        controller.refreshUsers(UserHandle.USER_NULL)
+    }
+
+    companion object {
+        @JvmStatic
+        protected val disabledUserAvatarColorFilter: ColorFilter by lazy {
+            val matrix = ColorMatrix()
+            matrix.setSaturation(0f) // 0 - grayscale
+            ColorMatrixColorFilter(matrix)
+        }
+
+        @JvmStatic
+        @JvmOverloads
+        protected fun getIconDrawable(
+            context: Context,
+            item: UserRecord,
+            isTablet: Boolean = false,
+        ): Drawable {
+            val iconRes =
+                getUserSwitcherActionIconResourceId(
+                    item.isAddUser,
+                    item.isGuest,
+                    item.isAddSupervisedUser,
+                    isTablet,
+                )
+            return checkNotNull(context.getDrawable(iconRes))
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
index 753e940..149ed0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryController.java
@@ -19,14 +19,17 @@
 import android.annotation.Nullable;
 import android.view.View;
 
-import com.android.systemui.Dumpable;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 
 import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 
-public interface BatteryController extends DemoMode, Dumpable,
+/**
+ * Controller for battery related information, including the charge level, power save mode,
+ * and time remaining information
+ */
+public interface BatteryController extends DemoMode,
         CallbackController<BatteryStateChangeCallback> {
     /**
      * Prints the current state of the {@link BatteryController} to the given {@link PrintWriter}.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
index 33ddf7e..c7ad767 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BatteryControllerImpl.java
@@ -38,11 +38,13 @@
 import com.android.settingslib.fuelgauge.BatterySaverUtils;
 import com.android.settingslib.fuelgauge.Estimate;
 import com.android.settingslib.utils.PowerUtil;
+import com.android.systemui.Dumpable;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.demomode.DemoMode;
 import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.power.EnhancedEstimates;
 import com.android.systemui.util.Assert;
 
@@ -56,7 +58,8 @@
  * Default implementation of a {@link BatteryController}. This controller monitors for battery
  * level change events that are broadcasted by the system.
  */
-public class BatteryControllerImpl extends BroadcastReceiver implements BatteryController {
+public class BatteryControllerImpl extends BroadcastReceiver implements BatteryController,
+        Dumpable {
     private static final String TAG = "BatteryController";
 
     private static final String ACTION_LEVEL_TEST = "com.android.systemui.BATTERY_LEVEL_TEST";
@@ -70,6 +73,7 @@
     private final ArrayList<EstimateFetchCompletion> mFetchCallbacks = new ArrayList<>();
     private final PowerManager mPowerManager;
     private final DemoModeController mDemoModeController;
+    private final DumpManager mDumpManager;
     private final Handler mMainHandler;
     private final Handler mBgHandler;
     protected final Context mContext;
@@ -101,6 +105,7 @@
             PowerManager powerManager,
             BroadcastDispatcher broadcastDispatcher,
             DemoModeController demoModeController,
+            DumpManager dumpManager,
             @Main Handler mainHandler,
             @Background Handler bgHandler) {
         mContext = context;
@@ -110,6 +115,7 @@
         mEstimates = enhancedEstimates;
         mBroadcastDispatcher = broadcastDispatcher;
         mDemoModeController = demoModeController;
+        mDumpManager = dumpManager;
     }
 
     private void registerReceiver() {
@@ -134,6 +140,7 @@
             }
         }
         mDemoModeController.addCallback(this);
+        mDumpManager.registerDumpable(TAG, this);
         updatePowerSave();
         updateEstimateInBackground();
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
index 16306081..dc73d1f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchController.java
@@ -69,7 +69,7 @@
     private final Context mContext;
     private Resources mResources;
     private final UserSwitcherController mUserSwitcherController;
-    private UserSwitcherController.BaseUserAdapter mAdapter;
+    private BaseUserSwitcherAdapter mAdapter;
     private final KeyguardStateController mKeyguardStateController;
     private final FalsingManager mFalsingManager;
     protected final SysuiStatusBarStateController mStatusBarStateController;
@@ -171,7 +171,7 @@
         mUserAvatarView = mView.findViewById(R.id.kg_multi_user_avatar);
         mUserAvatarViewWithBackground = mView.findViewById(
                 R.id.kg_multi_user_avatar_with_background);
-        mAdapter = new UserSwitcherController.BaseUserAdapter(mUserSwitcherController) {
+        mAdapter = new BaseUserSwitcherAdapter(mUserSwitcherController) {
             @Override
             public View getView(int position, View convertView, ViewGroup parent) {
                 return null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
index e2f5734..0995a00 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/KeyguardUserSwitcherController.java
@@ -16,9 +16,6 @@
 
 package com.android.systemui.statusbar.policy;
 
-import static com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA;
-import static com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA;
-
 import android.animation.Animator;
 import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
@@ -232,14 +229,8 @@
     }
 
     /**
-     * See:
+     * Returns {@code true} if the user switcher should be open by default on the lock screen.
      *
-     * <ul>
-     *   <li>{@link com.android.internal.R.bool.config_expandLockScreenUserSwitcher}</li>
-     *    <li>{@link UserSwitcherController.SIMPLE_USER_SWITCHER_GLOBAL_SETTING}</li>
-     * </ul>
-     *
-     * @return true if the user switcher should be open by default on the lock screen.
      * @see android.os.UserManager#isUserSwitcherEnabled()
      */
     public boolean isSimpleUserSwitcher() {
@@ -436,7 +427,7 @@
     }
 
     static class KeyguardUserAdapter extends
-            UserSwitcherController.BaseUserAdapter implements View.OnClickListener {
+            BaseUserSwitcherAdapter implements View.OnClickListener {
 
         private final Context mContext;
         private final Resources mResources;
@@ -514,9 +505,9 @@
                 v.bind(name, drawable, item.info.id);
             }
             v.setActivated(item.isCurrent);
-            v.setDisabledByAdmin(mController.isDisabledByAdmin(item));
+            v.setDisabledByAdmin(getController().isDisabledByAdmin(item));
             v.setEnabled(item.isSwitchToEnabled);
-            v.setAlpha(v.isEnabled() ? USER_SWITCH_ENABLED_ALPHA : USER_SWITCH_DISABLED_ALPHA);
+            UserSwitcherController.setSelectableAlpha(v);
 
             if (item.isCurrent) {
                 mCurrentUserView = v;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
new file mode 100644
index 0000000..843c232
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.kt
@@ -0,0 +1,182 @@
+/*
+ * 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.policy
+
+import android.annotation.UserIdInt
+import android.content.Intent
+import android.view.View
+import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin
+import com.android.systemui.Dumpable
+import com.android.systemui.qs.user.UserSwitchDialogController.DialogShower
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper
+import java.lang.ref.WeakReference
+import kotlinx.coroutines.flow.Flow
+
+/** Defines interface for a class that provides user switching functionality and state. */
+interface UserSwitcherController : Dumpable {
+
+    /** The current list of [UserRecord]. */
+    val users: ArrayList<UserRecord>
+
+    /** Whether the user switcher experience should use the simple experience. */
+    val isSimpleUserSwitcher: Boolean
+
+    /** Require a view for jank detection */
+    fun init(view: View)
+
+    /** The [UserRecord] of the current user or `null` when none. */
+    val currentUserRecord: UserRecord?
+
+    /** The name of the current user of the device or `null`, when none is selected. */
+    val currentUserName: String?
+
+    /**
+     * Notifies that a user has been selected.
+     *
+     * This will trigger the right user journeys to create a guest user, switch users, and/or
+     * navigate to the correct destination.
+     *
+     * If a user with the given ID is not found, this method is a no-op.
+     *
+     * @param userId The ID of the user to switch to.
+     * @param dialogShower An optional [DialogShower] in case we need to show dialogs.
+     */
+    fun onUserSelected(userId: Int, dialogShower: DialogShower?)
+
+    /** Whether it is allowed to add users while the device is locked. */
+    val isAddUsersFromLockScreenEnabled: Flow<Boolean>
+
+    /** Whether the guest user is configured to always be present on the device. */
+    val isGuestUserAutoCreated: Boolean
+
+    /** Whether the guest user is currently being reset. */
+    val isGuestUserResetting: Boolean
+
+    /** Creates and switches to the guest user. */
+    fun createAndSwitchToGuestUser(dialogShower: DialogShower?)
+
+    /** Shows the add user dialog. */
+    fun showAddUserDialog(dialogShower: DialogShower?)
+
+    /** Starts an activity to add a supervised user to the device. */
+    fun startSupervisedUserActivity()
+
+    /** Notifies when the display density or font scale has changed. */
+    fun onDensityOrFontScaleChanged()
+
+    /** Registers an adapter to notify when the users change. */
+    fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>)
+
+    /** Notifies the item for a user has been clicked. */
+    fun onUserListItemClicked(record: UserRecord, dialogShower: DialogShower?)
+
+    /**
+     * Removes guest user and switches to target user. The guest must be the current user and its id
+     * must be `guestUserId`.
+     *
+     * If `targetUserId` is `UserHandle.USER_NULL`, then create a new guest user in the foreground,
+     * and immediately switch to it. This is used for wiping the current guest and replacing it with
+     * a new one.
+     *
+     * If `targetUserId` is specified, then remove the guest in the background while switching to
+     * `targetUserId`.
+     *
+     * If device is configured with `config_guestUserAutoCreated`, then after guest user is removed,
+     * a new one is created in the background. This has no effect if `targetUserId` is
+     * `UserHandle.USER_NULL`.
+     *
+     * @param guestUserId id of the guest user to remove
+     * @param targetUserId id of the user to switch to after guest is removed. If
+     * `UserHandle.USER_NULL`, then switch immediately to the newly created guest user.
+     */
+    fun removeGuestUser(@UserIdInt guestUserId: Int, @UserIdInt targetUserId: Int)
+
+    /**
+     * Exits guest user and switches to previous non-guest user. The guest must be the current user.
+     *
+     * @param guestUserId user id of the guest user to exit
+     * @param targetUserId user id of the guest user to exit, set to UserHandle#USER_NULL when
+     * target user id is not known
+     * @param forceRemoveGuestOnExit true: remove guest before switching user, false: remove guest
+     * only if its ephemeral, else keep guest
+     */
+    fun exitGuestUser(
+        @UserIdInt guestUserId: Int,
+        @UserIdInt targetUserId: Int,
+        forceRemoveGuestOnExit: Boolean
+    )
+
+    /**
+     * Guarantee guest is present only if the device is provisioned. Otherwise, create a content
+     * observer to wait until the device is provisioned, then schedule the guest creation.
+     */
+    fun schedulePostBootGuestCreation()
+
+    /** Whether keyguard is showing. */
+    val isKeyguardShowing: Boolean
+
+    /** Returns the [EnforcedAdmin] for the given record, or `null` if there isn't one. */
+    fun getEnforcedAdmin(record: UserRecord): EnforcedAdmin?
+
+    /** Returns `true` if the given record is disabled by the admin; `false` otherwise. */
+    fun isDisabledByAdmin(record: UserRecord): Boolean
+
+    /** Starts an activity with the given [Intent]. */
+    fun startActivity(intent: Intent)
+
+    /**
+     * Refreshes users from UserManager.
+     *
+     * The pictures are only loaded if they have not been loaded yet.
+     *
+     * @param forcePictureLoadForId forces the picture of the given user to be reloaded.
+     */
+    fun refreshUsers(forcePictureLoadForId: Int)
+
+    /** Adds a subscriber to when user switches. */
+    fun addUserSwitchCallback(callback: UserSwitchCallback)
+
+    /** Removes a previously-added subscriber. */
+    fun removeUserSwitchCallback(callback: UserSwitchCallback)
+
+    /** Defines interface for classes that can be called back when the user is switched. */
+    fun interface UserSwitchCallback {
+        /** Notifies that the user has switched. */
+        fun onUserSwitched()
+    }
+
+    companion object {
+        /** Alpha value to apply to a user view in the user switcher when it's selectable. */
+        private const val ENABLED_ALPHA =
+            LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA
+
+        /** Alpha value to apply to a user view in the user switcher when it's not selectable. */
+        private const val DISABLED_ALPHA =
+            LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA
+
+        @JvmStatic
+        fun setSelectableAlpha(view: View) {
+            view.alpha =
+                if (view.isEnabled) {
+                    ENABLED_ALPHA
+                } else {
+                    DISABLED_ALPHA
+                }
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
new file mode 100644
index 0000000..12834f6
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerImpl.kt
@@ -0,0 +1,269 @@
+/*
+ * 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.policy
+
+import android.content.Intent
+import android.view.View
+import com.android.settingslib.RestrictedLockUtils
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.qs.user.UserSwitchDialogController
+import com.android.systemui.user.data.source.UserRecord
+import dagger.Lazy
+import java.io.PrintWriter
+import java.lang.ref.WeakReference
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+/** Implementation of [UserSwitcherController]. */
+class UserSwitcherControllerImpl
+@Inject
+constructor(
+    private val flags: FeatureFlags,
+    @Suppress("DEPRECATION") private val oldImpl: Lazy<UserSwitcherControllerOldImpl>,
+) : UserSwitcherController {
+
+    private val isNewImpl: Boolean
+        get() = flags.isEnabled(Flags.REFACTORED_USER_SWITCHER_CONTROLLER)
+    private val _oldImpl: UserSwitcherControllerOldImpl
+        get() = oldImpl.get()
+
+    private fun notYetImplemented(): Nothing {
+        error("Not yet implemented!")
+    }
+
+    override val users: ArrayList<UserRecord>
+        get() =
+            if (isNewImpl) {
+                notYetImplemented()
+            } else {
+                _oldImpl.users
+            }
+
+    override val isSimpleUserSwitcher: Boolean
+        get() =
+            if (isNewImpl) {
+                notYetImplemented()
+            } else {
+                _oldImpl.isSimpleUserSwitcher
+            }
+
+    override fun init(view: View) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.init(view)
+        }
+    }
+
+    override val currentUserRecord: UserRecord?
+        get() =
+            if (isNewImpl) {
+                notYetImplemented()
+            } else {
+                _oldImpl.currentUserRecord
+            }
+
+    override val currentUserName: String?
+        get() =
+            if (isNewImpl) {
+                notYetImplemented()
+            } else {
+                _oldImpl.currentUserName
+            }
+
+    override fun onUserSelected(
+        userId: Int,
+        dialogShower: UserSwitchDialogController.DialogShower?
+    ) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.onUserSelected(userId, dialogShower)
+        }
+    }
+
+    override val isAddUsersFromLockScreenEnabled: Flow<Boolean>
+        get() =
+            if (isNewImpl) {
+                notYetImplemented()
+            } else {
+                _oldImpl.isAddUsersFromLockScreenEnabled
+            }
+
+    override val isGuestUserAutoCreated: Boolean
+        get() =
+            if (isNewImpl) {
+                notYetImplemented()
+            } else {
+                _oldImpl.isGuestUserAutoCreated
+            }
+
+    override val isGuestUserResetting: Boolean
+        get() =
+            if (isNewImpl) {
+                notYetImplemented()
+            } else {
+                _oldImpl.isGuestUserResetting
+            }
+
+    override fun createAndSwitchToGuestUser(
+        dialogShower: UserSwitchDialogController.DialogShower?,
+    ) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.createAndSwitchToGuestUser(dialogShower)
+        }
+    }
+
+    override fun showAddUserDialog(dialogShower: UserSwitchDialogController.DialogShower?) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.showAddUserDialog(dialogShower)
+        }
+    }
+
+    override fun startSupervisedUserActivity() {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.startSupervisedUserActivity()
+        }
+    }
+
+    override fun onDensityOrFontScaleChanged() {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.onDensityOrFontScaleChanged()
+        }
+    }
+
+    override fun addAdapter(adapter: WeakReference<BaseUserSwitcherAdapter>) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.addAdapter(adapter)
+        }
+    }
+
+    override fun onUserListItemClicked(
+        record: UserRecord,
+        dialogShower: UserSwitchDialogController.DialogShower?,
+    ) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.onUserListItemClicked(record, dialogShower)
+        }
+    }
+
+    override fun removeGuestUser(guestUserId: Int, targetUserId: Int) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.removeGuestUser(guestUserId, targetUserId)
+        }
+    }
+
+    override fun exitGuestUser(
+        guestUserId: Int,
+        targetUserId: Int,
+        forceRemoveGuestOnExit: Boolean
+    ) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.exitGuestUser(guestUserId, targetUserId, forceRemoveGuestOnExit)
+        }
+    }
+
+    override fun schedulePostBootGuestCreation() {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.schedulePostBootGuestCreation()
+        }
+    }
+
+    override val isKeyguardShowing: Boolean
+        get() =
+            if (isNewImpl) {
+                notYetImplemented()
+            } else {
+                _oldImpl.isKeyguardShowing
+            }
+
+    override fun getEnforcedAdmin(record: UserRecord): RestrictedLockUtils.EnforcedAdmin? {
+        return if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.getEnforcedAdmin(record)
+        }
+    }
+
+    override fun isDisabledByAdmin(record: UserRecord): Boolean {
+        return if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.isDisabledByAdmin(record)
+        }
+    }
+
+    override fun startActivity(intent: Intent) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.startActivity(intent)
+        }
+    }
+
+    override fun refreshUsers(forcePictureLoadForId: Int) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.refreshUsers(forcePictureLoadForId)
+        }
+    }
+
+    override fun addUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.addUserSwitchCallback(callback)
+        }
+    }
+
+    override fun removeUserSwitchCallback(callback: UserSwitcherController.UserSwitchCallback) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.removeUserSwitchCallback(callback)
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        if (isNewImpl) {
+            notYetImplemented()
+        } else {
+            _oldImpl.dump(pw, args)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
similarity index 82%
rename from packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
rename to packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
index 1d5b88e7..d365aa6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImpl.java
@@ -11,9 +11,8 @@
  * 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
+ * limitations under the License.
  */
-
 package com.android.systemui.statusbar.policy;
 
 import static android.os.UserManager.SWITCHABILITY_STATUS_OK;
@@ -34,10 +33,6 @@
 import android.content.pm.UserInfo;
 import android.database.ContentObserver;
 import android.graphics.Bitmap;
-import android.graphics.ColorFilter;
-import android.graphics.ColorMatrix;
-import android.graphics.ColorMatrixColorFilter;
-import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -51,7 +46,6 @@
 import android.util.SparseBooleanArray;
 import android.view.View;
 import android.view.WindowManagerGlobal;
-import android.widget.BaseAdapter;
 import android.widget.Toast;
 
 import androidx.annotation.Nullable;
@@ -63,7 +57,6 @@
 import com.android.internal.util.LatencyTracker;
 import com.android.settingslib.RestrictedLockUtilsInternal;
 import com.android.settingslib.users.UserCreatingDialog;
-import com.android.systemui.Dumpable;
 import com.android.systemui.GuestResetOrExitSessionReceiver;
 import com.android.systemui.GuestResumeSessionReceiver;
 import com.android.systemui.R;
@@ -86,7 +79,6 @@
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.user.CreateUserActivity;
 import com.android.systemui.user.data.source.UserRecord;
-import com.android.systemui.user.legacyhelper.ui.LegacyUserUiHelper;
 import com.android.systemui.util.settings.GlobalSettings;
 import com.android.systemui.util.settings.SecureSettings;
 
@@ -106,15 +98,14 @@
 import kotlinx.coroutines.flow.StateFlowKt;
 
 /**
- * Keeps a list of all users on the device for user switching.
+ * Old implementation. Keeps a list of all users on the device for user switching.
+ *
+ * @deprecated This is the old implementation. Please depend on {@link UserSwitcherController}
+ * instead.
  */
+@Deprecated
 @SysUISingleton
-public class UserSwitcherController implements Dumpable {
-
-    public static final float USER_SWITCH_ENABLED_ALPHA =
-            LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_SELECTABLE_ALPHA;
-    public static final float USER_SWITCH_DISABLED_ALPHA =
-            LegacyUserUiHelper.USER_SWITCHER_USER_VIEW_NOT_SELECTABLE_ALPHA;
+public class UserSwitcherControllerOldImpl implements UserSwitcherController {
 
     private static final String TAG = "UserSwitcherController";
     private static final boolean DEBUG = false;
@@ -123,7 +114,7 @@
     private static final int PAUSE_REFRESH_USERS_TIMEOUT_MS = 3000;
 
     private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
-    private static final long MULTI_USER_JOURNEY_TIMEOUT = 20000l;
+    private static final long MULTI_USER_JOURNEY_TIMEOUT = 20000L;
 
     private static final String INTERACTION_JANK_ADD_NEW_USER_TAG = "add_new_user";
     private static final String INTERACTION_JANK_EXIT_GUEST_MODE_TAG = "exit_guest_mode";
@@ -132,7 +123,7 @@
     protected final UserTracker mUserTracker;
     protected final UserManager mUserManager;
     private final ContentObserver mSettingsObserver;
-    private final ArrayList<WeakReference<BaseUserAdapter>> mAdapters = new ArrayList<>();
+    private final ArrayList<WeakReference<BaseUserSwitcherAdapter>> mAdapters = new ArrayList<>();
     @VisibleForTesting
     final GuestResumeSessionReceiver mGuestResumeSessionReceiver;
     @VisibleForTesting
@@ -158,7 +149,6 @@
     @VisibleForTesting
     Dialog mAddUserDialog;
     private int mLastNonGuestUser = UserHandle.USER_SYSTEM;
-    private boolean mResumeUserOnGuestLogout = true;
     private boolean mSimpleUserSwitcher;
     // When false, there won't be any visual affordance to add a new user from the keyguard even if
     // the user is unlocked
@@ -187,7 +177,8 @@
             Collections.synchronizedList(new ArrayList<>());
 
     @Inject
-    public UserSwitcherController(Context context,
+    public UserSwitcherControllerOldImpl(
+            Context context,
             IActivityManager activityManager,
             UserManager userManager,
             UserTracker userTracker,
@@ -303,16 +294,10 @@
         refreshUsers(UserHandle.USER_NULL);
     }
 
-    /**
-     * Refreshes users from UserManager.
-     *
-     * The pictures are only loaded if they have not been loaded yet.
-     *
-     * @param forcePictureLoadForId forces the picture of the given user to be reloaded.
-     */
+    @Override
     @SuppressWarnings("unchecked")
-    private void refreshUsers(int forcePictureLoadForId) {
-        if (DEBUG) Log.d(TAG, "refreshUsers(forcePictureLoadForId=" + forcePictureLoadForId+")");
+    public void refreshUsers(int forcePictureLoadForId) {
+        if (DEBUG) Log.d(TAG, "refreshUsers(forcePictureLoadForId=" + forcePictureLoadForId + ")");
         if (forcePictureLoadForId != UserHandle.USER_NULL) {
             mForcePictureLoadForUserId.put(forcePictureLoadForId, true);
         }
@@ -323,8 +308,8 @@
 
         boolean forceAllUsers = mForcePictureLoadForUserId.get(UserHandle.USER_ALL);
         SparseArray<Bitmap> bitmaps = new SparseArray<>(mUsers.size());
-        final int N = mUsers.size();
-        for (int i = 0; i < N; i++) {
+        final int userCount = mUsers.size();
+        for (int i = 0; i < userCount; i++) {
             UserRecord r = mUsers.get(i);
             if (r == null || r.picture == null || r.info == null || forceAllUsers
                     || mForcePictureLoadForUserId.get(r.info.id)) {
@@ -431,38 +416,41 @@
         });
     }
 
-    boolean systemCanCreateUsers() {
+    private boolean systemCanCreateUsers() {
         return !mUserManager.hasBaseUserRestriction(
                 UserManager.DISALLOW_ADD_USER, UserHandle.SYSTEM);
     }
 
-    boolean currentUserCanCreateUsers() {
+    private boolean currentUserCanCreateUsers() {
         UserInfo currentUser = mUserTracker.getUserInfo();
         return currentUser != null
                 && (currentUser.isAdmin() || mUserTracker.getUserId() == UserHandle.USER_SYSTEM)
                 && systemCanCreateUsers();
     }
 
-    boolean anyoneCanCreateUsers() {
+    private boolean anyoneCanCreateUsers() {
         return systemCanCreateUsers() && mAddUsersFromLockScreen.getValue();
     }
 
+    @VisibleForTesting
     boolean canCreateGuest(boolean hasExistingGuest) {
         return mUserSwitcherEnabled
                 && (currentUserCanCreateUsers() || anyoneCanCreateUsers())
                 && !hasExistingGuest;
     }
 
+    @VisibleForTesting
     boolean canCreateUser() {
         return mUserSwitcherEnabled
                 && (currentUserCanCreateUsers() || anyoneCanCreateUsers())
                 && mUserManager.canAddMoreUsers(UserManager.USER_TYPE_FULL_SECONDARY);
     }
 
-    boolean createIsRestricted() {
+    private boolean createIsRestricted() {
         return !mAddUsersFromLockScreen.getValue();
     }
 
+    @VisibleForTesting
     boolean canCreateSupervisedUser() {
         return !TextUtils.isEmpty(mCreateSupervisedUserPackage) && canCreateUser();
     }
@@ -476,7 +464,7 @@
 
     private void notifyAdapters() {
         for (int i = mAdapters.size() - 1; i >= 0; i--) {
-            BaseUserAdapter adapter = mAdapters.get(i).get();
+            BaseUserSwitcherAdapter adapter = mAdapters.get(i).get();
             if (adapter != null) {
                 adapter.notifyDataSetChanged();
             } else {
@@ -485,37 +473,20 @@
         }
     }
 
+    @Override
     public boolean isSimpleUserSwitcher() {
         return mSimpleUserSwitcher;
     }
 
-    public void setResumeUserOnGuestLogout(boolean resume) {
-        mResumeUserOnGuestLogout = resume;
-    }
-
     /**
      * Returns whether the current user is a system user.
      */
-    public boolean isSystemUser() {
+    @VisibleForTesting
+    boolean isSystemUser() {
         return mUserTracker.getUserId() == UserHandle.USER_SYSTEM;
     }
 
-    public void removeUserId(int userId) {
-        if (userId == UserHandle.USER_SYSTEM) {
-            Log.w(TAG, "User " + userId + " could not removed.");
-            return;
-        }
-        if (mUserTracker.getUserId() == userId) {
-            switchToUserId(UserHandle.USER_SYSTEM);
-        }
-        if (mUserManager.removeUser(userId)) {
-            refreshUsers(UserHandle.USER_NULL);
-        }
-    }
-
-    /**
-     * @return UserRecord for the current user
-     */
+    @Override
     public @Nullable UserRecord getCurrentUserRecord() {
         for (int i = 0; i < mUsers.size(); ++i) {
             UserRecord userRecord = mUsers.get(i);
@@ -526,17 +497,7 @@
         return null;
     }
 
-    /**
-     * Notifies that a user has been selected.
-     *
-     * <p>This will trigger the right user journeys to create a guest user, switch users, and/or
-     * navigate to the correct destination.
-     *
-     * <p>If a user with the given ID is not found, this method is a no-op.
-     *
-     * @param userId The ID of the user to switch to.
-     * @param dialogShower An optional {@link DialogShower} in case we need to show dialogs.
-     */
+    @Override
     public void onUserSelected(int userId, @Nullable DialogShower dialogShower) {
         UserRecord userRecord = mUsers.stream()
                 .filter(x -> x.resolveId() == userId)
@@ -549,23 +510,23 @@
         onUserListItemClicked(userRecord, dialogShower);
     }
 
-    /** Whether it is allowed to add users while the device is locked. */
-    public Flow<Boolean> getAddUsersFromLockScreen() {
+    @Override
+    public Flow<Boolean> isAddUsersFromLockScreenEnabled() {
         return mAddUsersFromLockScreen;
     }
 
-    /** Returns {@code true} if the guest user is configured to always be present on the device. */
+    @Override
     public boolean isGuestUserAutoCreated() {
         return mGuestUserAutoCreated;
     }
 
-    /** Returns {@code true} if the guest user is currently being reset. */
+    @Override
     public boolean isGuestUserResetting() {
         return mGuestIsResetting.get();
     }
 
-    @VisibleForTesting
-    void onUserListItemClicked(UserRecord record, DialogShower dialogShower) {
+    @Override
+    public void onUserListItemClicked(UserRecord record, DialogShower dialogShower) {
         if (record.isGuest && record.info == null) {
             createAndSwitchToGuestUser(dialogShower);
         } else if (record.isAddUser) {
@@ -604,7 +565,7 @@
         switchToUserId(id);
     }
 
-    protected void switchToUserId(int id) {
+    private void switchToUserId(int id) {
         try {
             if (mView != null) {
                 mInteractionJankMonitor.begin(InteractionJankMonitor.Configuration.Builder
@@ -621,7 +582,7 @@
 
     private void showExitGuestDialog(int id, boolean isGuestEphemeral, DialogShower dialogShower) {
         int newId = UserHandle.USER_SYSTEM;
-        if (mResumeUserOnGuestLogout && mLastNonGuestUser != UserHandle.USER_SYSTEM) {
+        if (mLastNonGuestUser != UserHandle.USER_SYSTEM) {
             UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser);
             if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) {
                 newId = info.id;
@@ -645,9 +606,7 @@
         }
     }
 
-    /**
-     * Creates and switches to the guest user.
-     */
+    @Override
     public void createAndSwitchToGuestUser(@Nullable DialogShower dialogShower) {
         createGuestAsync(guestId -> {
             // guestId may be USER_NULL if we haven't reloaded the user list yet.
@@ -658,9 +617,7 @@
         });
     }
 
-    /**
-     * Shows the add user dialog.
-     */
+    @Override
     public void showAddUserDialog(@Nullable DialogShower dialogShower) {
         if (mAddUserDialog != null && mAddUserDialog.isShowing()) {
             mAddUserDialog.cancel();
@@ -677,9 +634,7 @@
         }
     }
 
-    /**
-     * Starts an activity to add a supervised user to the device.
-     */
+    @Override
     public void startSupervisedUserActivity() {
         final Intent intent = new Intent()
                 .setAction(UserManager.ACTION_CREATE_SUPERVISED_USER)
@@ -711,7 +666,7 @@
         public void onReceive(Context context, Intent intent) {
             if (DEBUG) {
                 Log.v(TAG, "Broadcast: a=" + intent.getAction()
-                       + " user=" + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1));
+                        + " user=" + intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1));
             }
 
             boolean unpauseRefreshUsers = false;
@@ -725,8 +680,8 @@
 
                 final int currentId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
                 final UserInfo userInfo = mUserManager.getUserInfo(currentId);
-                final int N = mUsers.size();
-                for (int i = 0; i < N; i++) {
+                final int userCount = mUsers.size();
+                for (int i = 0; i < userCount; i++) {
                     UserRecord record = mUsers.get(i);
                     if (record.info == null) continue;
                     boolean shouldBeCurrent = record.info.id == currentId;
@@ -805,7 +760,7 @@
         pw.println("mGuestUserAutoCreated=" + mGuestUserAutoCreated);
     }
 
-    /** Returns the name of the current user of the phone. */
+    @Override
     public String getCurrentUserName() {
         if (mUsers.isEmpty()) return null;
         UserRecord item = mUsers.stream().filter(x -> x.isCurrent).findFirst().orElse(null);
@@ -814,40 +769,22 @@
         return item.info.name;
     }
 
+    @Override
     public void onDensityOrFontScaleChanged() {
         refreshUsers(UserHandle.USER_ALL);
     }
 
-    @VisibleForTesting
-    public void addAdapter(WeakReference<BaseUserAdapter> adapter) {
+    @Override
+    public void addAdapter(WeakReference<BaseUserSwitcherAdapter> adapter) {
         mAdapters.add(adapter);
     }
 
-    @VisibleForTesting
+    @Override
     public ArrayList<UserRecord> getUsers() {
         return mUsers;
     }
 
-    /**
-     * Removes guest user and switches to target user. The guest must be the current user and its id
-     * must be {@code guestUserId}.
-     *
-     * <p>If {@code targetUserId} is {@link UserHandle#USER_NULL}, then create a new guest user in
-     * the foreground, and immediately switch to it. This is used for wiping the current guest and
-     * replacing it with a new one.
-     *
-     * <p>If {@code targetUserId} is specified, then remove the guest in the background while
-     * switching to {@code targetUserId}.
-     *
-     * <p>If device is configured with {@link
-     * com.android.internal.R.bool.config_guestUserAutoCreated}, then after guest user is removed, a
-     * new one is created in the background. This has no effect if {@code targetUserId} is {@link
-     * UserHandle#USER_NULL}.
-     *
-     * @param guestUserId id of the guest user to remove
-     * @param targetUserId id of the user to switch to after guest is removed. If {@link
-     * UserHandle#USER_NULL}, then switch immediately to the newly created guest user.
-     */
+    @Override
     public void removeGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId) {
         UserInfo currentUser = mUserTracker.getUserInfo();
         if (currentUser.id != guestUserId) {
@@ -894,18 +831,9 @@
         }
     }
 
-    /**
-     * Exits guest user and switches to previous non-guest user. The guest must be the current
-     * user.
-     *
-     * @param guestUserId user id of the guest user to exit
-     * @param targetUserId user id of the guest user to exit, set to UserHandle#USER_NULL when
-     *                       target user id is not known
-     * @param forceRemoveGuestOnExit true: remove guest before switching user,
-     *                               false: remove guest only if its ephemeral, else keep guest
-     */
+    @Override
     public void exitGuestUser(@UserIdInt int guestUserId, @UserIdInt int targetUserId,
-                    boolean forceRemoveGuestOnExit) {
+            boolean forceRemoveGuestOnExit) {
         UserInfo currentUser = mUserTracker.getUserInfo();
         if (currentUser.id != guestUserId) {
             Log.w(TAG, "User requesting to start a new session (" + guestUserId + ")"
@@ -921,7 +849,7 @@
         int newUserId = UserHandle.USER_SYSTEM;
         if (targetUserId == UserHandle.USER_NULL) {
             // when target user is not specified switch to last non guest user
-            if (mResumeUserOnGuestLogout && mLastNonGuestUser != UserHandle.USER_SYSTEM) {
+            if (mLastNonGuestUser != UserHandle.USER_SYSTEM) {
                 UserInfo info = mUserManager.getUserInfo(mLastNonGuestUser);
                 if (info != null && info.isEnabled() && info.supportsSwitchToByUser()) {
                     newUserId = info.id;
@@ -959,10 +887,7 @@
 
     }
 
-    /**
-     * Guarantee guest is present only if the device is provisioned. Otherwise, create a content
-     * observer to wait until the device is provisioned, then schedule the guest creation.
-     */
+    @Override
     public void schedulePostBootGuestCreation() {
         if (isDeviceAllowedToAddGuest()) {
             guaranteeGuestPresent();
@@ -1014,7 +939,7 @@
      * @return The multi-user user ID of the newly created guest user, or
      * {@link UserHandle#USER_NULL} if the guest couldn't be created.
      */
-    public @UserIdInt int createGuest() {
+    private @UserIdInt int createGuest() {
         UserInfo guest;
         try {
             guest = mUserManager.createGuest(mContext);
@@ -1029,135 +954,27 @@
         return guest.id;
     }
 
-    /**
-     * Require a view for jank detection
-     */
+    @Override
     public void init(View view) {
         mView = view;
     }
 
-    @VisibleForTesting
-    public KeyguardStateController getKeyguardStateController() {
-        return mKeyguardStateController;
+    @Override
+    public boolean isKeyguardShowing() {
+        return mKeyguardStateController.isShowing();
     }
 
-    /**
-     * Returns the {@link EnforcedAdmin} for the given record, or {@code null} if there isn't one.
-     */
+    @Override
     @Nullable
     public EnforcedAdmin getEnforcedAdmin(UserRecord record) {
         return mEnforcedAdminByUserRecord.get(record);
     }
 
-    /**
-     * Returns {@code true} if the given record is disabled by the admin; {@code false} otherwise.
-     */
+    @Override
     public boolean isDisabledByAdmin(UserRecord record) {
         return mDisabledByAdmin.contains(record);
     }
 
-    public static abstract class BaseUserAdapter extends BaseAdapter {
-
-        final UserSwitcherController mController;
-        private final KeyguardStateController mKeyguardStateController;
-
-        protected BaseUserAdapter(UserSwitcherController controller) {
-            mController = controller;
-            mKeyguardStateController = controller.getKeyguardStateController();
-            controller.addAdapter(new WeakReference<>(this));
-        }
-
-        protected ArrayList<UserRecord> getUsers() {
-            return mController.getUsers();
-        }
-
-        public int getUserCount() {
-            return countUsers(false);
-        }
-
-        @Override
-        public int getCount() {
-            return countUsers(true);
-        }
-
-        private int countUsers(boolean includeGuest) {
-            boolean keyguardShowing = mKeyguardStateController.isShowing();
-            final int userSize = getUsers().size();
-            int count = 0;
-            for (int i = 0; i < userSize; i++) {
-                if (getUsers().get(i).isGuest && !includeGuest) {
-                    continue;
-                }
-                if (getUsers().get(i).isRestricted && keyguardShowing) {
-                    break;
-                }
-                count++;
-            }
-            return count;
-        }
-
-        @Override
-        public UserRecord getItem(int position) {
-            return getUsers().get(position);
-        }
-
-        @Override
-        public long getItemId(int position) {
-            return position;
-        }
-
-        /**
-         * It handles click events on user list items.
-         *
-         * If the user switcher is hosted in a dialog, passing a non-null {@link DialogShower}
-         * will allow animation to and from the parent dialog.
-         *
-         */
-        public void onUserListItemClicked(UserRecord record, @Nullable DialogShower dialogShower) {
-            mController.onUserListItemClicked(record, dialogShower);
-        }
-
-        public void onUserListItemClicked(UserRecord record) {
-            onUserListItemClicked(record, null);
-        }
-
-        public String getName(Context context, UserRecord item) {
-            return getName(context, item, false);
-        }
-
-        /**
-         * Returns the name for the given {@link UserRecord}.
-         */
-        public String getName(Context context, UserRecord item, boolean isTablet) {
-            return LegacyUserUiHelper.getUserRecordName(
-                    context,
-                    item,
-                    mController.isGuestUserAutoCreated(),
-                    mController.isGuestUserResetting(),
-                    isTablet);
-        }
-
-        protected static ColorFilter getDisabledUserAvatarColorFilter() {
-            ColorMatrix matrix = new ColorMatrix();
-            matrix.setSaturation(0f);   // 0 - grayscale
-            return new ColorMatrixColorFilter(matrix);
-        }
-
-        protected static Drawable getIconDrawable(Context context, UserRecord item) {
-            return getIconDrawable(context, item, false);
-        }
-        protected static Drawable getIconDrawable(Context context, UserRecord item,
-                boolean isTablet) {
-            int iconRes = LegacyUserUiHelper.getUserSwitcherActionIconResourceId(
-                    item.isAddUser, item.isGuest, item.isAddSupervisedUser, isTablet);
-            return context.getDrawable(iconRes);
-        }
-
-        public void refresh() {
-            mController.refreshUsers(UserHandle.USER_NULL);
-        }
-    }
-
     private void checkIfAddUserDisallowedByAdminOnly(UserRecord record) {
         EnforcedAdmin admin = RestrictedLockUtilsInternal.checkIfRestrictionEnforced(mContext,
                 UserManager.DISALLOW_ADD_USER, mUserTracker.getUserId());
@@ -1178,20 +995,17 @@
                 defaultSimpleUserSwitcher, UserHandle.USER_SYSTEM) != 0;
     }
 
+    @Override
     public void startActivity(Intent intent) {
         mActivityStarter.startActivity(intent, true);
     }
 
-    /**
-     *  Add a subscriber to when user switches.
-     */
+    @Override
     public void addUserSwitchCallback(UserSwitchCallback callback) {
         mUserSwitchCallbacks.add(callback);
     }
 
-    /**
-     *  Remove a subscriber to when user switches.
-     */
+    @Override
     public void removeUserSwitchCallback(UserSwitchCallback callback) {
         mUserSwitchCallbacks.remove(callback);
     }
@@ -1218,7 +1032,7 @@
                     // which
                     // helps making the transition faster.
                     if (!mKeyguardStateController.isShowing()) {
-                        mHandler.post(UserSwitcherController.this::notifyAdapters);
+                        mHandler.post(UserSwitcherControllerOldImpl.this::notifyAdapters);
                     } else {
                         notifyAdapters();
                     }
@@ -1367,13 +1181,4 @@
         }
     }
 
-    /**
-     * Callback to for when this controller receives the intent to switch users.
-     */
-    public interface UserSwitchCallback {
-        /**
-         * Called when user has switched.
-         */
-        void onUserSwitched();
-    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
index 1b73539..b1b45b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/dagger/StatusBarPolicyModule.java
@@ -58,6 +58,8 @@
 import com.android.systemui.statusbar.policy.SecurityControllerImpl;
 import com.android.systemui.statusbar.policy.UserInfoController;
 import com.android.systemui.statusbar.policy.UserInfoControllerImpl;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.statusbar.policy.UserSwitcherControllerImpl;
 import com.android.systemui.statusbar.policy.WalletController;
 import com.android.systemui.statusbar.policy.WalletControllerImpl;
 import com.android.systemui.statusbar.policy.ZenModeController;
@@ -196,4 +198,8 @@
     static DataSaverController provideDataSaverController(NetworkController networkController) {
         return networkController.getDataSaverController();
     }
+
+    /** Binds {@link UserSwitcherController} to its implementation. */
+    @Binds
+    UserSwitcherController bindUserSwitcherController(UserSwitcherControllerImpl impl);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
index 27746c0..00ed3d6 100644
--- a/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
+++ b/packages/SystemUI/src/com/android/systemui/tv/TvSystemUIModule.java
@@ -36,6 +36,7 @@
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dock.DockManagerImpl;
 import com.android.systemui.doze.DozeHost;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.navigationbar.gestural.GestureModule;
 import com.android.systemui.plugins.qs.QSFactory;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -116,9 +117,12 @@
     static BatteryController provideBatteryController(Context context,
             EnhancedEstimates enhancedEstimates, PowerManager powerManager,
             BroadcastDispatcher broadcastDispatcher, DemoModeController demoModeController,
+            DumpManager dumpManager,
             @Main Handler mainHandler, @Background Handler bgHandler) {
         BatteryController bC = new BatteryControllerImpl(context, enhancedEstimates, powerManager,
-                broadcastDispatcher, demoModeController, mainHandler, bgHandler);
+                broadcastDispatcher, demoModeController,
+                dumpManager,
+                mainHandler, bgHandler);
         bC.init();
         return bC;
     }
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
index 5e2dde6..108ab43 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserSwitcherActivity.kt
@@ -53,10 +53,8 @@
 import com.android.systemui.plugins.FalsingManager
 import com.android.systemui.plugins.FalsingManager.LOW_PENALTY
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter
 import com.android.systemui.statusbar.policy.UserSwitcherController
-import com.android.systemui.statusbar.policy.UserSwitcherController.BaseUserAdapter
-import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_DISABLED_ALPHA
-import com.android.systemui.statusbar.policy.UserSwitcherController.USER_SWITCH_ENABLED_ALPHA
 import com.android.systemui.user.data.source.UserRecord
 import com.android.systemui.user.ui.binder.UserSwitcherViewBinder
 import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
@@ -66,10 +64,10 @@
 
 private const val USER_VIEW = "user_view"
 
-/**
- * Support a fullscreen user switcher
- */
-open class UserSwitcherActivity @Inject constructor(
+/** Support a fullscreen user switcher */
+open class UserSwitcherActivity
+@Inject
+constructor(
     private val userSwitcherController: UserSwitcherController,
     private val broadcastDispatcher: BroadcastDispatcher,
     private val falsingCollector: FalsingCollector,
@@ -86,11 +84,12 @@
     private lateinit var addButton: View
     private var addUserRecords = mutableListOf<UserRecord>()
     private val onBackCallback = OnBackInvokedCallback { finish() }
-    private val userSwitchedCallback: UserTracker.Callback = object : UserTracker.Callback {
-        override fun onUserChanged(newUser: Int, userContext: Context) {
-            finish()
+    private val userSwitchedCallback: UserTracker.Callback =
+        object : UserTracker.Callback {
+            override fun onUserChanged(newUser: Int, userContext: Context) {
+                finish()
+            }
         }
-    }
     // When the add users options become available, insert another option to manage users
     private val manageUserRecord =
         UserRecord(
@@ -114,13 +113,14 @@
     @VisibleForTesting
     fun createActivity() {
         setContentView(R.layout.user_switcher_fullscreen)
-        window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
-                or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
-                or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
+        window.decorView.systemUiVisibility =
+            (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
+                View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or
+                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION)
         if (isUsingModernArchitecture()) {
             Log.d(TAG, "Using modern architecture.")
-            val viewModel = ViewModelProvider(
-                this, viewModelFactory.get())[UserSwitcherViewModel::class.java]
+            val viewModel =
+                ViewModelProvider(this, viewModelFactory.get())[UserSwitcherViewModel::class.java]
             UserSwitcherViewBinder.bind(
                 view = requireViewById(R.id.user_switcher_root),
                 viewModel = viewModel,
@@ -136,27 +136,23 @@
 
         parent = requireViewById<UserSwitcherRootView>(R.id.user_switcher_root)
 
-        parent.touchHandler = object : Gefingerpoken {
-            override fun onTouchEvent(ev: MotionEvent?): Boolean {
-                falsingCollector.onTouchEvent(ev)
-                return false
+        parent.touchHandler =
+            object : Gefingerpoken {
+                override fun onTouchEvent(ev: MotionEvent?): Boolean {
+                    falsingCollector.onTouchEvent(ev)
+                    return false
+                }
             }
-        }
 
-        requireViewById<View>(R.id.cancel).apply {
-            setOnClickListener {
-                _ -> finish()
-            }
-        }
+        requireViewById<View>(R.id.cancel).apply { setOnClickListener { _ -> finish() } }
 
-        addButton = requireViewById<View>(R.id.add).apply {
-            setOnClickListener {
-                _ -> showPopupMenu()
-            }
-        }
+        addButton =
+            requireViewById<View>(R.id.add).apply { setOnClickListener { _ -> showPopupMenu() } }
 
         onBackInvokedDispatcher.registerOnBackInvokedCallback(
-                OnBackInvokedDispatcher.PRIORITY_DEFAULT, onBackCallback)
+            OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+            onBackCallback
+        )
 
         userSwitcherController.init(parent)
         initBroadcastReceiver()
@@ -169,25 +165,30 @@
         val items = mutableListOf<UserRecord>()
         addUserRecords.forEach { items.add(it) }
 
-        var popupMenuAdapter = ItemAdapter(
-            this,
-            R.layout.user_switcher_fullscreen_popup_item,
-            layoutInflater,
-            { item: UserRecord -> adapter.getName(this@UserSwitcherActivity, item, true) },
-            { item: UserRecord -> adapter.findUserIcon(item, true).mutate().apply {
-                setTint(resources.getColor(
-                    R.color.user_switcher_fullscreen_popup_item_tint,
-                    getTheme()
-                ))
-            } }
-        )
+        var popupMenuAdapter =
+            ItemAdapter(
+                this,
+                R.layout.user_switcher_fullscreen_popup_item,
+                layoutInflater,
+                { item: UserRecord -> adapter.getName(this@UserSwitcherActivity, item, true) },
+                { item: UserRecord ->
+                    adapter.findUserIcon(item, true).mutate().apply {
+                        setTint(
+                            resources.getColor(
+                                R.color.user_switcher_fullscreen_popup_item_tint,
+                                getTheme()
+                            )
+                        )
+                    }
+                }
+            )
         popupMenuAdapter.addAll(items)
 
-        popupMenu = UserSwitcherPopupMenu(this).apply {
-            setAnchorView(addButton)
-            setAdapter(popupMenuAdapter)
-            setOnItemClickListener {
-                parent: AdapterView<*>, view: View, pos: Int, id: Long ->
+        popupMenu =
+            UserSwitcherPopupMenu(this).apply {
+                setAnchorView(addButton)
+                setAdapter(popupMenuAdapter)
+                setOnItemClickListener { parent: AdapterView<*>, view: View, pos: Int, id: Long ->
                     if (falsingManager.isFalseTap(LOW_PENALTY) || !view.isEnabled()) {
                         return@setOnItemClickListener
                     }
@@ -206,10 +207,10 @@
                     if (!item.isAddUser) {
                         this@UserSwitcherActivity.finish()
                     }
-            }
+                }
 
-            show()
-        }
+                show()
+            }
     }
 
     private fun buildUserViews() {
@@ -227,8 +228,8 @@
         val totalWidth = parent.width
         val userViewCount = adapter.getTotalUserViews()
         val maxColumns = getMaxColumns(userViewCount)
-        val horizontalGap = resources
-            .getDimensionPixelSize(R.dimen.user_switcher_fullscreen_horizontal_gap)
+        val horizontalGap =
+            resources.getDimensionPixelSize(R.dimen.user_switcher_fullscreen_horizontal_gap)
         val totalWidthOfHorizontalGap = (maxColumns - 1) * horizontalGap
         val maxWidgetDiameter = (totalWidth - totalWidthOfHorizontalGap) / maxColumns
 
@@ -299,14 +300,15 @@
     }
 
     private fun initBroadcastReceiver() {
-        broadcastReceiver = object : BroadcastReceiver() {
-            override fun onReceive(context: Context, intent: Intent) {
-                val action = intent.getAction()
-                if (Intent.ACTION_SCREEN_OFF.equals(action)) {
-                    finish()
+        broadcastReceiver =
+            object : BroadcastReceiver() {
+                override fun onReceive(context: Context, intent: Intent) {
+                    val action = intent.getAction()
+                    if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+                        finish()
+                    }
                 }
             }
-        }
 
         val filter = IntentFilter()
         filter.addAction(Intent.ACTION_SCREEN_OFF)
@@ -322,9 +324,7 @@
         return flags.isEnabled(Flags.MODERN_USER_SWITCHER_ACTIVITY)
     }
 
-    /**
-     * Provides views to populate the option menu.
-     */
+    /** Provides views to populate the option menu. */
     private class ItemAdapter(
         val parentContext: Context,
         val resource: Int,
@@ -337,43 +337,27 @@
             val item = getItem(position)
             val view = convertView ?: layoutInflater.inflate(resource, parent, false)
 
-            view.requireViewById<ImageView>(R.id.icon).apply {
-                setImageDrawable(iconGetter(item))
-            }
-            view.requireViewById<TextView>(R.id.text).apply {
-                setText(textGetter(item))
-            }
+            view.requireViewById<ImageView>(R.id.icon).apply { setImageDrawable(iconGetter(item)) }
+            view.requireViewById<TextView>(R.id.text).apply { setText(textGetter(item)) }
 
             return view
         }
     }
 
-    private inner class UserAdapter : BaseUserAdapter(userSwitcherController) {
+    private inner class UserAdapter : BaseUserSwitcherAdapter(userSwitcherController) {
         override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
             val item = getItem(position)
             var view = convertView as ViewGroup?
             if (view == null) {
-                view = layoutInflater.inflate(
-                    R.layout.user_switcher_fullscreen_item,
-                    parent,
-                    false
-                ) as ViewGroup
+                view =
+                    layoutInflater.inflate(R.layout.user_switcher_fullscreen_item, parent, false)
+                        as ViewGroup
             }
-            (view.getChildAt(0) as ImageView).apply {
-                setImageDrawable(getDrawable(item))
-            }
-            (view.getChildAt(1) as TextView).apply {
-                setText(getName(getContext(), item))
-            }
+            (view.getChildAt(0) as ImageView).apply { setImageDrawable(getDrawable(item)) }
+            (view.getChildAt(1) as TextView).apply { setText(getName(getContext(), item)) }
 
             view.setEnabled(item.isSwitchToEnabled)
-            view.setAlpha(
-                if (view.isEnabled()) {
-                    USER_SWITCH_ENABLED_ALPHA
-                } else {
-                    USER_SWITCH_DISABLED_ALPHA
-                }
-            )
+            UserSwitcherController.setSelectableAlpha(view)
             view.setTag(USER_VIEW)
             return view
         }
@@ -401,23 +385,20 @@
         }
 
         fun getTotalUserViews(): Int {
-            return users.count { item ->
-                !doNotRenderUserView(item)
-            }
+            return users.count { item -> !doNotRenderUserView(item) }
         }
 
         fun doNotRenderUserView(item: UserRecord): Boolean {
-            return item.isAddUser ||
-                    item.isAddSupervisedUser ||
-                    item.isGuest && item.info == null
+            return item.isAddUser || item.isAddSupervisedUser || item.isGuest && item.info == null
         }
 
         private fun getDrawable(item: UserRecord): Drawable {
-            var drawable = if (item.isGuest) {
-                getDrawable(R.drawable.ic_account_circle)
-            } else {
-                findUserIcon(item)
-            }
+            var drawable =
+                if (item.isGuest) {
+                    getDrawable(R.drawable.ic_account_circle)
+                } else {
+                    findUserIcon(item)
+                }
             drawable.mutate()
 
             if (!item.isCurrent && !item.isSwitchToEnabled) {
@@ -429,16 +410,16 @@
                 )
             }
 
-            val ld = getDrawable(R.drawable.user_switcher_icon_large).mutate()
-                    as LayerDrawable
-            if (item == userSwitcherController.getCurrentUserRecord()) {
+            val ld = getDrawable(R.drawable.user_switcher_icon_large).mutate() as LayerDrawable
+            if (item == userSwitcherController.currentUserRecord) {
                 (ld.findDrawableByLayerId(R.id.ring) as GradientDrawable).apply {
-                    val stroke = resources
-                        .getDimensionPixelSize(R.dimen.user_switcher_icon_selected_width)
-                    val color = Utils.getColorAttrDefaultColor(
-                        this@UserSwitcherActivity,
-                        com.android.internal.R.attr.colorAccentPrimary
-                    )
+                    val stroke =
+                        resources.getDimensionPixelSize(R.dimen.user_switcher_icon_selected_width)
+                    val color =
+                        Utils.getColorAttrDefaultColor(
+                            this@UserSwitcherActivity,
+                            com.android.internal.R.attr.colorAccentPrimary
+                        )
 
                     setStroke(stroke, color)
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index 305b5ee..0356388 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -99,7 +99,7 @@
     override val actions: Flow<List<UserActionModel>> =
         userRecords.map { records -> records.filter { it.isNotUser() }.map { it.toActionModel() } }
 
-    override val isActionableWhenLocked: Flow<Boolean> = controller.addUsersFromLockScreen
+    override val isActionableWhenLocked: Flow<Boolean> = controller.isAddUsersFromLockScreenEnabled
 
     override val isGuestUserAutoCreated: Boolean = controller.isGuestUserAutoCreated
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
index d7ad3ce..938417f 100644
--- a/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/ui/binder/UserSwitcherViewBinder.kt
@@ -174,6 +174,7 @@
             setOnItemClickListener { _, _, position, _ ->
                 val itemPositionExcludingHeader = position - 1
                 adapter.getItem(itemPositionExcludingHeader).onClicked()
+                dismiss()
             }
 
             show()
diff --git a/packages/SystemUI/src/com/android/systemui/util/recycler/HorizontalSpacerItemDecoration.kt b/packages/SystemUI/src/com/android/systemui/util/recycler/HorizontalSpacerItemDecoration.kt
new file mode 100644
index 0000000..ac931e5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/recycler/HorizontalSpacerItemDecoration.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.util.recycler
+
+import android.graphics.Rect
+import android.view.View
+import androidx.annotation.Dimension
+import androidx.recyclerview.widget.RecyclerView
+
+/**
+ * RecyclerView ItemDecorator that adds a horizontal space of the given size between items
+ * and double that space on the ends.
+ */
+class HorizontalSpacerItemDecoration(@Dimension private val offset: Int) :
+    RecyclerView.ItemDecoration() {
+
+    override fun getItemOffsets(
+        outRect: Rect,
+        view: View,
+        parent: RecyclerView,
+        state: RecyclerView.State
+    ) {
+        val position: Int = parent.getChildAdapterPosition(view)
+        val itemCount = parent.adapter?.itemCount ?: 0
+
+        val left = if (position == 0) offset * 2 else offset
+        val right = if (position == itemCount - 1) offset * 2 else offset
+
+        outRect.set(left, 0, right, 0)
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
index 2714cf4..aca60c0 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
@@ -86,14 +86,12 @@
     becauseCannotSkipBouncer = false,
     biometricSettingEnabledForUser = false,
     bouncerFullyShown = false,
-    bouncerIsOrWillShow = false,
-    onlyFaceEnrolled = false,
     faceAuthenticated = false,
     faceDisabled = false,
     faceLockedOut = false,
     fpLockedOut = false,
     goingToSleep = false,
-    keyguardAwakeExcludingBouncerShowing = false,
+    keyguardAwake = false,
     keyguardGoingAway = false,
     listeningForFaceAssistant = false,
     occludingAppRequestingFaceAuth = false,
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
index 28e99da..43f6f1a 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerTest.java
@@ -116,9 +116,7 @@
                 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
 
         when(mUserSwitcherController.getCurrentUserName()).thenReturn("Test User");
-        when(mUserSwitcherController.getKeyguardStateController())
-                .thenReturn(mKeyguardStateController);
-        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mUserSwitcherController.isKeyguardShowing()).thenReturn(true);
 
         mScreenWidth = getUiDevice().getDisplayWidth();
         mFakeMeasureSpec = View
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index ae980f5..9c64c1b 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -212,8 +212,6 @@
             mBiometricEnabledCallbackArgCaptor;
     @Captor
     private ArgumentCaptor<FaceManager.AuthenticationCallback> mAuthenticationCallbackCaptor;
-    @Captor
-    private ArgumentCaptor<CancellationSignal> mCancellationSignalCaptor;
 
     // Direct executor
     private final Executor mBackgroundExecutor = Runnable::run;
@@ -596,13 +594,11 @@
 
     @Test
     public void testTriesToAuthenticate_whenBouncer() {
-        fingerprintIsNotEnrolled();
-        faceAuthEnabled();
         setKeyguardBouncerVisibility(true);
 
         verify(mFaceManager).authenticate(any(), any(), any(), any(), anyInt(), anyBoolean());
-        verify(mFaceManager, atLeastOnce()).isHardwareDetected();
-        verify(mFaceManager, atLeastOnce()).hasEnrolledTemplates(anyInt());
+        verify(mFaceManager).isHardwareDetected();
+        verify(mFaceManager).hasEnrolledTemplates(anyInt());
     }
 
     @Test
@@ -1237,9 +1233,7 @@
     public void testShouldListenForFace_whenFaceIsAlreadyAuthenticated_returnsFalse()
             throws RemoteException {
         // Face auth should run when the following is true.
-        faceAuthEnabled();
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         keyguardNotGoingAway();
         currentUserIsPrimary();
         strongAuthNotRequired();
@@ -1266,7 +1260,7 @@
         mKeyguardUpdateMonitor =
                 new TestableKeyguardUpdateMonitor(mSpiedContext);
 
-        // Preconditions for face auth to run
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
         strongAuthNotRequired();
@@ -1283,7 +1277,7 @@
     @Test
     public void testShouldListenForFace_whenStrongAuthDoesNotAllowScanning_returnsFalse()
             throws RemoteException {
-        // Preconditions for face auth to run
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
         currentUserIsPrimary();
@@ -1304,11 +1298,8 @@
     @Test
     public void testShouldListenForFace_whenBiometricsDisabledForUser_returnsFalse()
             throws RemoteException {
-        // Preconditions for face auth to run
-        faceAuthEnabled();
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
         biometricsNotDisabledThroughDevicePolicyManager();
@@ -1328,11 +1319,9 @@
     @Test
     public void testShouldListenForFace_whenUserCurrentlySwitching_returnsFalse()
             throws RemoteException {
-        // Preconditions for face auth to run
-        faceAuthEnabled();
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
         biometricsNotDisabledThroughDevicePolicyManager();
@@ -1351,11 +1340,8 @@
     @Test
     public void testShouldListenForFace_whenSecureCameraLaunched_returnsFalse()
             throws RemoteException {
-        // Preconditions for face auth to run
-        faceAuthEnabled();
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
         biometricsNotDisabledThroughDevicePolicyManager();
@@ -1374,7 +1360,7 @@
     @Test
     public void testShouldListenForFace_whenOccludingAppRequestsFaceAuth_returnsTrue()
             throws RemoteException {
-        // Preconditions for face auth to run
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         bouncerFullyVisibleAndNotGoingToSleep();
         currentUserIsPrimary();
@@ -1397,8 +1383,7 @@
     @Test
     public void testShouldListenForFace_whenBouncerShowingAndDeviceIsAwake_returnsTrue()
             throws RemoteException {
-        // Preconditions for face auth to run
-        faceAuthEnabled();
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
@@ -1410,7 +1395,6 @@
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
 
         bouncerFullyVisibleAndNotGoingToSleep();
-        fingerprintIsNotEnrolled();
         mTestableLooper.processAllMessages();
 
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
@@ -1419,7 +1403,7 @@
     @Test
     public void testShouldListenForFace_whenAuthInterruptIsActive_returnsTrue()
             throws RemoteException {
-        // Preconditions for face auth to run
+        // Face auth should run when the following is true.
         keyguardNotGoingAway();
         currentUserIsPrimary();
         currentUserDoesNotHaveTrust();
@@ -1445,7 +1429,6 @@
         biometricsNotDisabledThroughDevicePolicyManager();
         biometricsEnabledForCurrentUser();
         userNotCurrentlySwitching();
-        bouncerFullyVisible();
 
         statusBarShadeIsLocked();
         mTestableLooper.processAllMessages();
@@ -1459,9 +1442,6 @@
         keyguardIsVisible();
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
         statusBarShadeIsNotLocked();
-        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
-        bouncerNotFullyVisible();
-
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
     }
 
@@ -1523,44 +1503,6 @@
     }
 
     @Test
-    public void testBouncerVisibility_whenBothFingerprintAndFaceIsEnrolled_stopsFaceAuth()
-            throws RemoteException {
-        // Both fingerprint and face are enrolled by default
-        // Preconditions for face auth to run
-        keyguardNotGoingAway();
-        currentUserIsPrimary();
-        currentUserDoesNotHaveTrust();
-        biometricsNotDisabledThroughDevicePolicyManager();
-        biometricsEnabledForCurrentUser();
-        userNotCurrentlySwitching();
-        deviceNotGoingToSleep();
-        deviceIsInteractive();
-        statusBarShadeIsNotLocked();
-        keyguardIsVisible();
-
-        mTestableLooper.processAllMessages();
-        clearInvocations(mUiEventLogger);
-
-        assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
-
-        mKeyguardUpdateMonitor.requestFaceAuth(true,
-                FaceAuthApiRequestReason.UDFPS_POINTER_DOWN);
-
-        verify(mFaceManager).authenticate(any(),
-                mCancellationSignalCaptor.capture(),
-                mAuthenticationCallbackCaptor.capture(),
-                any(),
-                anyInt(),
-                anyBoolean());
-        CancellationSignal cancelSignal = mCancellationSignalCaptor.getValue();
-
-        bouncerWillBeVisibleSoon();
-        mTestableLooper.processAllMessages();
-
-        assertThat(cancelSignal.isCanceled()).isTrue();
-    }
-
-    @Test
     public void testFingerprintCanAuth_whenCancellationNotReceivedAndAuthFailed() {
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
         mTestableLooper.processAllMessages();
@@ -1623,21 +1565,6 @@
                 .onAuthenticationError(FaceManager.FACE_ERROR_LOCKOUT_PERMANENT, "");
     }
 
-    private void faceAuthEnabled() {
-        // this ensures KeyguardUpdateMonitor updates the cached mIsFaceEnrolled flag using the
-        // face manager mock wire-up in setup()
-        mKeyguardUpdateMonitor.isFaceAuthEnabledForUser(mCurrentUserId);
-    }
-
-    private void fingerprintIsNotEnrolled() {
-        when(mFingerprintManager.hasEnrolledTemplates(mCurrentUserId)).thenReturn(false);
-        // This updates the cached fingerprint state.
-        // There is no straightforward API to update the fingerprint state.
-        // It currently works updates after enrollment changes because something else invokes
-        // startListeningForFingerprint(), which internally calls this method.
-        mKeyguardUpdateMonitor.isUnlockWithFingerprintPossible(mCurrentUserId);
-    }
-
     private void statusBarShadeIsNotLocked() {
         mStatusBarStateListener.onStateChanged(StatusBarState.KEYGUARD);
     }
@@ -1744,19 +1671,10 @@
         mKeyguardUpdateMonitor.dispatchStartedWakingUp();
     }
 
-    private void bouncerNotFullyVisible() {
-        setKeyguardBouncerVisibility(false);
-    }
-
     private void bouncerFullyVisible() {
         setKeyguardBouncerVisibility(true);
     }
 
-    private void bouncerWillBeVisibleSoon() {
-        mKeyguardUpdateMonitor.sendKeyguardBouncerChanged(true, false);
-        mTestableLooper.processAllMessages();
-    }
-
     private void setKeyguardBouncerVisibility(boolean isVisible) {
         mKeyguardUpdateMonitor.sendKeyguardBouncerChanged(isVisible, isVisible);
         mTestableLooper.processAllMessages();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java
index 8e00d10..faa5db4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/DistanceClassifierTest.java
@@ -17,7 +17,7 @@
 package com.android.systemui.classifier;
 
 import static com.android.systemui.classifier.Classifier.BRIGHTNESS_SLIDER;
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 
 import static com.google.common.truth.Truth.assertThat;
 
@@ -106,9 +106,9 @@
     }
 
     @Test
-    public void testPass_QsSwipeAlwaysPasses() {
+    public void testPass_QsSwipeSideAlwaysPasses() {
         mClassifier.onTouchEvent(appendDownEvent(1, 1));
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 1).isFalse())
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 1).isFalse())
                 .isFalse();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java
index 1d61e29..d70d6fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/classifier/TypeClassifierTest.java
@@ -22,7 +22,8 @@
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DISMISS;
 import static com.android.systemui.classifier.Classifier.NOTIFICATION_DRAG_DOWN;
 import static com.android.systemui.classifier.Classifier.PULSE_EXPAND;
-import static com.android.systemui.classifier.Classifier.QS_SWIPE;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_NESTED;
+import static com.android.systemui.classifier.Classifier.QS_SWIPE_SIDE;
 import static com.android.systemui.classifier.Classifier.QUICK_SETTINGS;
 import static com.android.systemui.classifier.Classifier.RIGHT_AFFORDANCE;
 import static com.android.systemui.classifier.Classifier.UNLOCK;
@@ -323,44 +324,86 @@
     }
 
     @Test
-    public void testPass_QsSwipe() {
+    public void testPass_QsSwipeSide() {
         when(mDataProvider.isVertical()).thenReturn(false);
 
         when(mDataProvider.isUp()).thenReturn(false);  // up and right should cause no effect.
         when(mDataProvider.isRight()).thenReturn(false);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isFalse();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isFalse();
 
         when(mDataProvider.isUp()).thenReturn(true);
         when(mDataProvider.isRight()).thenReturn(false);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isFalse();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isFalse();
 
         when(mDataProvider.isUp()).thenReturn(false);
         when(mDataProvider.isRight()).thenReturn(true);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isFalse();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isFalse();
 
         when(mDataProvider.isUp()).thenReturn(true);
         when(mDataProvider.isRight()).thenReturn(true);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isFalse();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isFalse();
     }
 
     @Test
-    public void testFalse_QsSwipe() {
+    public void testFalse_QsSwipeSide() {
         when(mDataProvider.isVertical()).thenReturn(true);
 
         when(mDataProvider.isUp()).thenReturn(false);  // up and right should cause no effect.
         when(mDataProvider.isRight()).thenReturn(false);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isTrue();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isTrue();
 
         when(mDataProvider.isUp()).thenReturn(true);
         when(mDataProvider.isRight()).thenReturn(false);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isTrue();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isTrue();
 
         when(mDataProvider.isUp()).thenReturn(false);
         when(mDataProvider.isRight()).thenReturn(true);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isTrue();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isTrue();
 
         when(mDataProvider.isUp()).thenReturn(true);
         when(mDataProvider.isRight()).thenReturn(true);
-        assertThat(mClassifier.classifyGesture(QS_SWIPE, 0.5, 0).isFalse()).isTrue();
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_SIDE, 0.5, 0).isFalse()).isTrue();
+    }
+
+    @Test
+    public void testPass_QsNestedSwipe() {
+        when(mDataProvider.isVertical()).thenReturn(true);
+
+        when(mDataProvider.isUp()).thenReturn(false);  // up and right should cause no effect.
+        when(mDataProvider.isRight()).thenReturn(false);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isFalse();
+
+        when(mDataProvider.isUp()).thenReturn(true);
+        when(mDataProvider.isRight()).thenReturn(false);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isFalse();
+
+        when(mDataProvider.isUp()).thenReturn(false);
+        when(mDataProvider.isRight()).thenReturn(true);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isFalse();
+
+        when(mDataProvider.isUp()).thenReturn(true);
+        when(mDataProvider.isRight()).thenReturn(true);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isFalse();
+    }
+
+    @Test
+    public void testFalse_QsNestedSwipe() {
+        when(mDataProvider.isVertical()).thenReturn(false);
+
+        when(mDataProvider.isUp()).thenReturn(false);  // up and right should cause no effect.
+        when(mDataProvider.isRight()).thenReturn(false);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isTrue();
+
+        when(mDataProvider.isUp()).thenReturn(true);
+        when(mDataProvider.isRight()).thenReturn(false);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isTrue();
+
+        when(mDataProvider.isUp()).thenReturn(false);
+        when(mDataProvider.isRight()).thenReturn(true);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isTrue();
+
+        when(mDataProvider.isUp()).thenReturn(true);
+        when(mDataProvider.isRight()).thenReturn(true);
+        assertThat(mClassifier.classifyGesture(QS_SWIPE_NESTED, 0.5, 0).isFalse()).isTrue();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index d70467d..c5a7de4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -36,6 +36,7 @@
 import com.android.keyguard.BouncerPanelExpansionCalculator;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.complication.ComplicationHostViewController;
+import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
 import com.android.systemui.statusbar.BlurUtils;
 import com.android.systemui.statusbar.phone.KeyguardBouncer;
 import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback;
@@ -88,6 +89,9 @@
     @Mock
     ViewRootImpl mViewRoot;
 
+    @Mock
+    BouncerCallbackInteractor mBouncerCallbackInteractor;
+
     DreamOverlayContainerViewController mController;
 
     @Before
@@ -110,7 +114,8 @@
                 mResources,
                 MAX_BURN_IN_OFFSET,
                 BURN_IN_PROTECTION_UPDATE_INTERVAL,
-                MILLIS_UNTIL_FULL_JITTER);
+                MILLIS_UNTIL_FULL_JITTER,
+                mBouncerCallbackInteractor);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
index 9d4275e..eec33ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -23,8 +23,10 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.ComponentName;
 import android.content.Intent;
 import android.os.IBinder;
+import android.os.RemoteException;
 import android.service.dreams.DreamService;
 import android.service.dreams.IDreamOverlay;
 import android.service.dreams.IDreamOverlayCallback;
@@ -57,6 +59,8 @@
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 public class DreamOverlayServiceTest extends SysuiTestCase {
+    private static final ComponentName LOW_LIGHT_COMPONENT = new ComponentName("package",
+            "lowlight");
     private final FakeSystemClock mFakeSystemClock = new FakeSystemClock();
     private final FakeExecutor mMainExecutor = new FakeExecutor(mFakeSystemClock);
 
@@ -129,7 +133,8 @@
                 mDreamOverlayComponentFactory,
                 mStateController,
                 mKeyguardUpdateMonitor,
-                mUiEventLogger);
+                mUiEventLogger,
+                LOW_LIGHT_COMPONENT);
     }
 
     @Test
@@ -204,6 +209,22 @@
     }
 
     @Test
+    public void testLowLightSetByIntentExtra() throws RemoteException {
+        final Intent intent = new Intent();
+        intent.putExtra(DreamService.EXTRA_DREAM_COMPONENT, LOW_LIGHT_COMPONENT);
+
+        final IBinder proxy = mService.onBind(intent);
+        final IDreamOverlay overlay = IDreamOverlay.Stub.asInterface(proxy);
+        assertThat(mService.getDreamComponent()).isEqualTo(LOW_LIGHT_COMPONENT);
+
+        // Inform the overlay service of dream starting.
+        overlay.startDream(mWindowParams, mDreamOverlayCallback);
+        mMainExecutor.runAllReady();
+
+        verify(mStateController).setLowLightActive(true);
+    }
+
+    @Test
     public void testDestroy() {
         mService.onDestroy();
         mMainExecutor.runAllReady();
@@ -211,6 +232,7 @@
         verify(mKeyguardUpdateMonitor).removeCallback(any());
         verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
         verify(mStateController).setOverlayActive(false);
+        verify(mStateController).setLowLightActive(false);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
index 2adf285..d1d32a1 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -218,4 +218,20 @@
         assertThat(stateController.getComplications(true).contains(complication))
                 .isTrue();
     }
+
+    @Test
+    public void testNotifyLowLightChanged() {
+        final DreamOverlayStateController stateController =
+                new DreamOverlayStateController(mExecutor);
+
+        stateController.addCallback(mCallback);
+        mExecutor.runAllReady();
+        assertThat(stateController.isLowLightActive()).isFalse();
+
+        stateController.setLowLightActive(true);
+
+        mExecutor.runAllReady();
+        verify(mCallback, times(1)).onStateChanged();
+        assertThat(stateController.isLowLightActive()).isTrue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
index 4ebae98..aa02178 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -101,6 +102,8 @@
     DreamOverlayStatusBarItemsProvider.StatusBarItem mStatusBarItem;
     @Mock
     View mStatusBarItemView;
+    @Mock
+    DreamOverlayStateController mDreamOverlayStateController;
 
     private final Executor mMainExecutor = Runnable::run;
 
@@ -126,7 +129,8 @@
                 Optional.of(mDreamOverlayNotificationCountProvider),
                 mZenModeController,
                 mStatusBarWindowStateController,
-                mDreamOverlayStatusBarItemsProvider);
+                mDreamOverlayStatusBarItemsProvider,
+                mDreamOverlayStateController);
     }
 
     @Test
@@ -137,6 +141,7 @@
         verify(mZenModeController).addCallback(any());
         verify(mDreamOverlayNotificationCountProvider).addCallback(any());
         verify(mDreamOverlayStatusBarItemsProvider).addCallback(any());
+        verify(mDreamOverlayStateController).addCallback(any());
     }
 
     @Test
@@ -266,7 +271,8 @@
                 Optional.empty(),
                 mZenModeController,
                 mStatusBarWindowStateController,
-                mDreamOverlayStatusBarItemsProvider);
+                mDreamOverlayStatusBarItemsProvider,
+                mDreamOverlayStateController);
         controller.onViewAttached();
         verify(mView, never()).showIcon(
                 eq(DreamOverlayStatusBarView.STATUS_ICON_NOTIFICATIONS), eq(true), any());
@@ -305,6 +311,7 @@
         verify(mZenModeController).removeCallback(any());
         verify(mDreamOverlayNotificationCountProvider).removeCallback(any());
         verify(mDreamOverlayStatusBarItemsProvider).removeCallback(any());
+        verify(mDreamOverlayStateController).removeCallback(any());
     }
 
     @Test
@@ -458,6 +465,7 @@
     @Test
     public void testStatusBarShownWhenSystemStatusBarHidden() {
         mController.onViewAttached();
+        reset(mView);
 
         final ArgumentCaptor<StatusBarWindowStateListener>
                 callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
@@ -471,6 +479,7 @@
     public void testUnattachedStatusBarVisibilityUnchangedWhenSystemStatusBarHidden() {
         mController.onViewAttached();
         mController.onViewDetached();
+        reset(mView);
 
         final ArgumentCaptor<StatusBarWindowStateListener>
                 callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
@@ -493,4 +502,21 @@
 
         verify(mView).setExtraStatusBarItemViews(List.of(mStatusBarItemView));
     }
+
+    @Test
+    public void testLowLightHidesStatusBar() {
+        when(mDreamOverlayStateController.isLowLightActive()).thenReturn(true);
+        mController.onViewAttached();
+
+        verify(mView).setVisibility(View.INVISIBLE);
+        reset(mView);
+
+        when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false);
+        final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture =
+                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+        verify(mDreamOverlayStateController).addCallback(callbackCapture.capture());
+        callbackCapture.getValue().onStateChanged();
+
+        verify(mView).setVisibility(View.VISIBLE);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
index bc94440..522b5b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/DreamMediaEntryComplicationTest.java
@@ -16,17 +16,28 @@
 
 package com.android.systemui.dreams.complication;
 
-import static org.mockito.Mockito.verify;
+import static com.android.systemui.flags.Flags.DREAM_MEDIA_TAP_TO_OPEN;
 
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.content.Intent;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.view.View;
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.ActivityIntentHelper;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.media.MediaCarouselController;
 import com.android.systemui.media.dream.MediaDreamComplication;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.statusbar.NotificationLockscreenUserManager;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -48,21 +59,52 @@
     @Mock
     private MediaDreamComplication mMediaComplication;
 
+    @Mock
+    private MediaCarouselController mMediaCarouselController;
+
+    @Mock
+    private ActivityStarter mActivityStarter;
+
+    @Mock
+    private ActivityIntentHelper mActivityIntentHelper;
+
+    @Mock
+    private KeyguardStateController mKeyguardStateController;
+
+    @Mock
+    private NotificationLockscreenUserManager mLockscreenUserManager;
+
+    @Mock
+    private FeatureFlags mFeatureFlags;
+
+    @Mock
+    private PendingIntent mPendingIntent;
+
+    private final Intent mIntent = new Intent("android.test.TEST_ACTION");
+    private final Integer mCurrentUserId = 99;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(false);
     }
 
     /**
      * Ensures clicking media entry chip adds/removes media complication.
      */
     @Test
-    public void testClick() {
+    public void testClickToOpenUMO() {
         final DreamMediaEntryComplication.DreamMediaEntryViewController viewController =
                 new DreamMediaEntryComplication.DreamMediaEntryViewController(
                         mView,
                         mDreamOverlayStateController,
-                        mMediaComplication);
+                        mMediaComplication,
+                        mMediaCarouselController,
+                        mActivityStarter,
+                        mActivityIntentHelper,
+                        mKeyguardStateController,
+                        mLockscreenUserManager,
+                        mFeatureFlags);
 
         final ArgumentCaptor<View.OnClickListener> clickListenerCaptor =
                 ArgumentCaptor.forClass(View.OnClickListener.class);
@@ -85,10 +127,90 @@
                 new DreamMediaEntryComplication.DreamMediaEntryViewController(
                         mView,
                         mDreamOverlayStateController,
-                        mMediaComplication);
+                        mMediaComplication,
+                        mMediaCarouselController,
+                        mActivityStarter,
+                        mActivityIntentHelper,
+                        mKeyguardStateController,
+                        mLockscreenUserManager,
+                        mFeatureFlags);
 
         viewController.onViewDetached();
         verify(mView).setSelected(false);
         verify(mDreamOverlayStateController).removeComplication(mMediaComplication);
     }
+
+    /**
+     * Ensures clicking media entry chip opens media when flag is set.
+     */
+    @Test
+    public void testClickToOpenMediaOverLockscreen() {
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(true);
+
+        when(mMediaCarouselController.getCurrentVisibleMediaContentIntent()).thenReturn(
+                mPendingIntent);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mPendingIntent.getIntent()).thenReturn(mIntent);
+        when(mLockscreenUserManager.getCurrentUserId()).thenReturn(mCurrentUserId);
+
+        final DreamMediaEntryComplication.DreamMediaEntryViewController viewController =
+                new DreamMediaEntryComplication.DreamMediaEntryViewController(
+                        mView,
+                        mDreamOverlayStateController,
+                        mMediaComplication,
+                        mMediaCarouselController,
+                        mActivityStarter,
+                        mActivityIntentHelper,
+                        mKeyguardStateController,
+                        mLockscreenUserManager,
+                        mFeatureFlags);
+        viewController.onViewAttached();
+
+        final ArgumentCaptor<View.OnClickListener> clickListenerCaptor =
+                ArgumentCaptor.forClass(View.OnClickListener.class);
+        verify(mView).setOnClickListener(clickListenerCaptor.capture());
+
+        when(mActivityIntentHelper.wouldShowOverLockscreen(mIntent, mCurrentUserId)).thenReturn(
+                true);
+
+        clickListenerCaptor.getValue().onClick(mView);
+        verify(mActivityStarter).startActivity(mIntent, true, null, true);
+    }
+
+    /**
+     * Ensures clicking media entry chip opens media when flag is set.
+     */
+    @Test
+    public void testClickToOpenMediaDismissingLockscreen() {
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_TAP_TO_OPEN)).thenReturn(true);
+
+        when(mMediaCarouselController.getCurrentVisibleMediaContentIntent()).thenReturn(
+                mPendingIntent);
+        when(mKeyguardStateController.isShowing()).thenReturn(true);
+        when(mPendingIntent.getIntent()).thenReturn(mIntent);
+        when(mLockscreenUserManager.getCurrentUserId()).thenReturn(mCurrentUserId);
+
+        final DreamMediaEntryComplication.DreamMediaEntryViewController viewController =
+                new DreamMediaEntryComplication.DreamMediaEntryViewController(
+                        mView,
+                        mDreamOverlayStateController,
+                        mMediaComplication,
+                        mMediaCarouselController,
+                        mActivityStarter,
+                        mActivityIntentHelper,
+                        mKeyguardStateController,
+                        mLockscreenUserManager,
+                        mFeatureFlags);
+        viewController.onViewAttached();
+
+        final ArgumentCaptor<View.OnClickListener> clickListenerCaptor =
+                ArgumentCaptor.forClass(View.OnClickListener.class);
+        verify(mView).setOnClickListener(clickListenerCaptor.capture());
+
+        when(mActivityIntentHelper.wouldShowOverLockscreen(mIntent, mCurrentUserId)).thenReturn(
+                false);
+
+        clickListenerCaptor.getValue().onClick(mView);
+        verify(mActivityStarter).postStartActivityDismissingKeyguard(mPendingIntent, null);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractorTest.kt
new file mode 100644
index 0000000..3a61c57
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerCallbackInteractorTest.kt
@@ -0,0 +1,86 @@
+/*
+ * 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.interactor
+
+import android.view.View
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+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.MockitoAnnotations
+
+@SmallTest
+@RunWith(JUnit4::class)
+class BouncerCallbackInteractorTest : SysuiTestCase() {
+    private val bouncerCallbackInteractor = BouncerCallbackInteractor()
+    @Mock private lateinit var bouncerExpansionCallback: KeyguardBouncer.BouncerExpansionCallback
+    @Mock private lateinit var keyguardResetCallback: KeyguardBouncer.KeyguardResetCallback
+
+    @Before
+    fun setup() {
+        MockitoAnnotations.initMocks(this)
+        bouncerCallbackInteractor.addBouncerExpansionCallback(bouncerExpansionCallback)
+        bouncerCallbackInteractor.addKeyguardResetCallback(keyguardResetCallback)
+    }
+
+    @Test
+    fun testOnFullyShown() {
+        bouncerCallbackInteractor.dispatchFullyShown()
+        verify(bouncerExpansionCallback).onFullyShown()
+    }
+
+    @Test
+    fun testOnFullyHidden() {
+        bouncerCallbackInteractor.dispatchFullyHidden()
+        verify(bouncerExpansionCallback).onFullyHidden()
+    }
+
+    @Test
+    fun testOnExpansionChanged() {
+        bouncerCallbackInteractor.dispatchExpansionChanged(5f)
+        verify(bouncerExpansionCallback).onExpansionChanged(5f)
+    }
+
+    @Test
+    fun testOnVisibilityChanged() {
+        bouncerCallbackInteractor.dispatchVisibilityChanged(View.INVISIBLE)
+        verify(bouncerExpansionCallback).onVisibilityChanged(false)
+    }
+
+    @Test
+    fun testOnStartingToHide() {
+        bouncerCallbackInteractor.dispatchStartingToHide()
+        verify(bouncerExpansionCallback).onStartingToHide()
+    }
+
+    @Test
+    fun testOnStartingToShow() {
+        bouncerCallbackInteractor.dispatchStartingToShow()
+        verify(bouncerExpansionCallback).onStartingToShow()
+    }
+
+    @Test
+    fun testOnKeyguardReset() {
+        bouncerCallbackInteractor.dispatchReset()
+        verify(keyguardResetCallback).onKeyguardReset()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt
new file mode 100644
index 0000000..e6c8dd8
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractorTest.kt
@@ -0,0 +1,278 @@
+/*
+ * 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.interactor
+
+import android.os.Looper
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.DejankUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.classifier.FalsingCollector
+import com.android.systemui.keyguard.DismissCallbackRegistry
+import com.android.systemui.keyguard.data.BouncerView
+import com.android.systemui.keyguard.data.repository.KeyguardBouncerRepository
+import com.android.systemui.keyguard.shared.model.BouncerCallbackActionsModel
+import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
+import com.android.systemui.keyguard.shared.model.KeyguardBouncerModel
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_HIDDEN
+import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.mockito.any
+import com.android.systemui.utils.os.FakeHandler
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Answers
+import org.mockito.Mock
+import org.mockito.Mockito.mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
+import org.mockito.MockitoAnnotations
+
+@SmallTest
+@RunWithLooper(setAsMainLooper = true)
+@RunWith(AndroidTestingRunner::class)
+class BouncerInteractorTest : SysuiTestCase() {
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS)
+    private lateinit var repository: KeyguardBouncerRepository
+    @Mock(answer = Answers.RETURNS_DEEP_STUBS) private lateinit var bouncerView: BouncerView
+    @Mock private lateinit var keyguardStateController: KeyguardStateController
+    @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
+    @Mock private lateinit var bouncerCallbackInteractor: BouncerCallbackInteractor
+    @Mock private lateinit var falsingCollector: FalsingCollector
+    @Mock private lateinit var dismissCallbackRegistry: DismissCallbackRegistry
+    @Mock private lateinit var keyguardBypassController: KeyguardBypassController
+    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    private val mainHandler = FakeHandler(Looper.getMainLooper())
+    private lateinit var bouncerInteractor: BouncerInteractor
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        DejankUtils.setImmediate(true)
+        bouncerInteractor =
+            BouncerInteractor(
+                repository,
+                bouncerView,
+                mainHandler,
+                keyguardStateController,
+                keyguardSecurityModel,
+                bouncerCallbackInteractor,
+                falsingCollector,
+                dismissCallbackRegistry,
+                keyguardBypassController,
+                keyguardUpdateMonitor,
+            )
+        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
+        `when`(repository.show.value).thenReturn(null)
+    }
+
+    @Test
+    fun testShow_isScrimmed() {
+        bouncerInteractor.show(true)
+        verify(repository).setShowMessage(null)
+        verify(repository).setOnScreenTurnedOff(false)
+        verify(repository).setKeyguardAuthenticated(null)
+        verify(repository).setHide(false)
+        verify(repository).setStartingToHide(false)
+        verify(repository).setScrimmed(true)
+        verify(repository).setExpansion(EXPANSION_VISIBLE)
+        verify(repository).setShowingSoon(true)
+        verify(keyguardStateController).notifyBouncerShowing(true)
+        verify(bouncerCallbackInteractor).dispatchStartingToShow()
+        verify(repository).setVisible(true)
+        verify(repository).setShow(any(KeyguardBouncerModel::class.java))
+        verify(repository).setShowingSoon(false)
+    }
+
+    @Test
+    fun testShow_isNotScrimmed() {
+        verify(repository, never()).setExpansion(EXPANSION_VISIBLE)
+    }
+
+    @Test
+    fun testShow_keyguardIsDone() {
+        `when`(bouncerView.delegate?.showNextSecurityScreenOrFinish()).thenReturn(true)
+        verify(keyguardStateController, never()).notifyBouncerShowing(true)
+        verify(bouncerCallbackInteractor, never()).dispatchStartingToShow()
+    }
+
+    @Test
+    fun testHide() {
+        bouncerInteractor.hide()
+        verify(falsingCollector).onBouncerHidden()
+        verify(keyguardStateController).notifyBouncerShowing(false)
+        verify(repository).setShowingSoon(false)
+        verify(repository).setOnDismissAction(null)
+        verify(repository).setVisible(false)
+        verify(repository).setHide(true)
+        verify(repository).setShow(null)
+    }
+
+    @Test
+    fun testExpansion() {
+        `when`(repository.expansionAmount.value).thenReturn(0.5f)
+        bouncerInteractor.setExpansion(0.6f)
+        verify(repository).setExpansion(0.6f)
+        verify(bouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
+    }
+
+    @Test
+    fun testExpansion_fullyShown() {
+        `when`(repository.expansionAmount.value).thenReturn(0.5f)
+        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
+        bouncerInteractor.setExpansion(EXPANSION_VISIBLE)
+        verify(falsingCollector).onBouncerShown()
+        verify(bouncerCallbackInteractor).dispatchFullyShown()
+    }
+
+    @Test
+    fun testExpansion_fullyHidden() {
+        `when`(repository.expansionAmount.value).thenReturn(0.5f)
+        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
+        bouncerInteractor.setExpansion(EXPANSION_HIDDEN)
+        verify(repository).setVisible(false)
+        verify(repository).setShow(null)
+        verify(falsingCollector).onBouncerHidden()
+        verify(bouncerCallbackInteractor).dispatchReset()
+        verify(bouncerCallbackInteractor).dispatchFullyHidden()
+    }
+
+    @Test
+    fun testExpansion_startingToHide() {
+        `when`(repository.expansionAmount.value).thenReturn(EXPANSION_VISIBLE)
+        bouncerInteractor.setExpansion(0.1f)
+        verify(repository).setStartingToHide(true)
+        verify(bouncerCallbackInteractor).dispatchStartingToHide()
+    }
+
+    @Test
+    fun testShowMessage() {
+        bouncerInteractor.showMessage("abc", null)
+        verify(repository).setShowMessage(BouncerShowMessageModel("abc", null))
+    }
+
+    @Test
+    fun testDismissAction() {
+        val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
+        val cancelAction = mock(Runnable::class.java)
+        bouncerInteractor.setDismissAction(onDismissAction, cancelAction)
+        verify(repository)
+            .setOnDismissAction(BouncerCallbackActionsModel(onDismissAction, cancelAction))
+    }
+
+    @Test
+    fun testUpdateResources() {
+        bouncerInteractor.updateResources()
+        verify(repository).setResourceUpdateRequests(true)
+    }
+
+    @Test
+    fun testNotifyKeyguardAuthenticated() {
+        bouncerInteractor.notifyKeyguardAuthenticated(true)
+        verify(repository).setKeyguardAuthenticated(true)
+    }
+
+    @Test
+    fun testOnScreenTurnedOff() {
+        bouncerInteractor.onScreenTurnedOff()
+        verify(repository).setOnScreenTurnedOff(true)
+    }
+
+    @Test
+    fun testSetKeyguardPosition() {
+        bouncerInteractor.setKeyguardPosition(0f)
+        verify(repository).setKeyguardPosition(0f)
+    }
+
+    @Test
+    fun testNotifyKeyguardAuthenticatedHandled() {
+        bouncerInteractor.notifyKeyguardAuthenticatedHandled()
+        verify(repository).setKeyguardAuthenticated(null)
+    }
+
+    @Test
+    fun testNotifyUpdatedResources() {
+        bouncerInteractor.notifyUpdatedResources()
+        verify(repository).setResourceUpdateRequests(false)
+    }
+
+    @Test
+    fun testSetBackButtonEnabled() {
+        bouncerInteractor.setBackButtonEnabled(true)
+        verify(repository).setIsBackButtonEnabled(true)
+    }
+
+    @Test
+    fun testStartDisappearAnimation() {
+        val runnable = mock(Runnable::class.java)
+        bouncerInteractor.startDisappearAnimation(runnable)
+        verify(repository).setStartDisappearAnimation(any(Runnable::class.java))
+    }
+
+    @Test
+    fun testIsFullShowing() {
+        `when`(repository.isVisible.value).thenReturn(true)
+        `when`(repository.expansionAmount.value).thenReturn(EXPANSION_VISIBLE)
+        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
+        assertThat(bouncerInteractor.isFullyShowing()).isTrue()
+        `when`(repository.isVisible.value).thenReturn(false)
+        assertThat(bouncerInteractor.isFullyShowing()).isFalse()
+    }
+
+    @Test
+    fun testIsScrimmed() {
+        `when`(repository.isScrimmed.value).thenReturn(true)
+        assertThat(bouncerInteractor.isScrimmed()).isTrue()
+        `when`(repository.isScrimmed.value).thenReturn(false)
+        assertThat(bouncerInteractor.isScrimmed()).isFalse()
+    }
+
+    @Test
+    fun testIsInTransit() {
+        `when`(repository.showingSoon.value).thenReturn(true)
+        assertThat(bouncerInteractor.isInTransit()).isTrue()
+        `when`(repository.showingSoon.value).thenReturn(false)
+        assertThat(bouncerInteractor.isInTransit()).isFalse()
+        `when`(repository.expansionAmount.value).thenReturn(0.5f)
+        assertThat(bouncerInteractor.isInTransit()).isTrue()
+    }
+
+    @Test
+    fun testIsAnimatingAway() {
+        `when`(repository.startingDisappearAnimation.value).thenReturn(Runnable {})
+        assertThat(bouncerInteractor.isAnimatingAway()).isTrue()
+        `when`(repository.startingDisappearAnimation.value).thenReturn(null)
+        assertThat(bouncerInteractor.isAnimatingAway()).isFalse()
+    }
+
+    @Test
+    fun testWillDismissWithAction() {
+        `when`(repository.onDismissAction.value?.onDismissAction)
+            .thenReturn(mock(ActivityStarter.OnDismissAction::class.java))
+        assertThat(bouncerInteractor.willDismissWithAction()).isTrue()
+        `when`(repository.onDismissAction.value?.onDismissAction).thenReturn(null)
+        assertThat(bouncerInteractor.willDismissWithAction()).isFalse()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
index 5dd1cfc..e3e3b74 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/MediaCarouselControllerTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.media
 
+import android.app.PendingIntent
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import androidx.test.filters.SmallTest
@@ -43,6 +44,7 @@
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyNoMoreInteractions
 import org.mockito.Mockito.`when` as whenever
@@ -366,7 +368,7 @@
                 playerIndex,
                 mediaCarouselController.mediaCarouselScrollHandler.visibleMediaIndex
         )
-        assertEquals( playerIndex, 0)
+        assertEquals(playerIndex, 0)
 
         // Replaying the same media player one more time.
         // And check that the card stays in its position.
@@ -402,4 +404,44 @@
         visualStabilityCallback.value.onReorderingAllowed()
         assertEquals(true, result)
     }
+
+    @Test
+    fun testGetCurrentVisibleMediaContentIntent() {
+        val clickIntent1 = mock(PendingIntent::class.java)
+        val player1 = Triple("player1",
+                DATA.copy(clickIntent = clickIntent1),
+                1000L)
+        clock.setCurrentTimeMillis(player1.third)
+        MediaPlayerData.addMediaPlayer(player1.first,
+                player1.second.copy(notificationKey = player1.first),
+                panel, clock, isSsReactivated = false)
+
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent1)
+
+        val clickIntent2 = mock(PendingIntent::class.java)
+        val player2 = Triple("player2",
+                DATA.copy(clickIntent = clickIntent2),
+                2000L)
+        clock.setCurrentTimeMillis(player2.third)
+        MediaPlayerData.addMediaPlayer(player2.first,
+                player2.second.copy(notificationKey = player2.first),
+                panel, clock, isSsReactivated = false)
+
+        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
+        // added to the front because it was active more recently.
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
+
+        val clickIntent3 = mock(PendingIntent::class.java)
+        val player3 = Triple("player3",
+                DATA.copy(clickIntent = clickIntent3),
+                500L)
+        clock.setCurrentTimeMillis(player3.third)
+        MediaPlayerData.addMediaPlayer(player3.first,
+                player3.second.copy(notificationKey = player3.first),
+                panel, clock, isSsReactivated = false)
+
+        // mediaCarouselScrollHandler.visibleMediaIndex is unchanged (= 0), and the new player is
+        // added to the end because it was active less recently.
+        assertEquals(mediaCarouselController.getCurrentVisibleMediaContentIntent(), clickIntent2)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
index 22ecb4b..5f64336 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java
@@ -23,6 +23,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.app.WallpaperColors;
+import android.graphics.Bitmap;
 import android.graphics.drawable.Icon;
 import android.testing.AndroidTestingRunner;
 import android.view.View;
@@ -102,6 +104,18 @@
     }
 
     @Test
+    public void getItemId_validPosition_returnCorrespondingId() {
+        assertThat(mMediaOutputAdapter.getItemId(0)).isEqualTo(mMediaDevices.get(
+                0).getId().hashCode());
+    }
+
+    @Test
+    public void getItemId_invalidPosition_returnPosition() {
+        int invalidPosition = mMediaDevices.size() + 1;
+        assertThat(mMediaOutputAdapter.getItemId(invalidPosition)).isEqualTo(invalidPosition);
+    }
+
+    @Test
     public void onBindViewHolder_bindPairNew_verifyView() {
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 2);
 
@@ -155,6 +169,33 @@
     }
 
     @Test
+    public void onBindViewHolder_bindConnectedDeviceWithMutingExpectedDeviceExist_verifyView() {
+        when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true);
+        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
+    public void onBindViewHolder_isMutingExpectedDevice_verifyView() {
+        when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(true);
+        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+        when(mMediaOutputController.isActiveRemoteDevice(mMediaDevice1)).thenReturn(false);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
+    }
+
+    @Test
     public void onBindViewHolder_initSeekbar_setsVolume() {
         when(mMediaDevice1.getMaxVolume()).thenReturn(TEST_MAX_VOLUME);
         when(mMediaDevice1.getCurrentVolume()).thenReturn(TEST_CURRENT_VOLUME);
@@ -165,6 +206,20 @@
     }
 
     @Test
+    public void onBindViewHolder_bindSelectableDevice_verifyView() {
+        List<MediaDevice> selectableDevices = new ArrayList<>();
+        selectableDevices.add(mMediaDevice2);
+        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_2);
+    }
+
+    @Test
     public void onBindViewHolder_bindNonActiveConnectedDevice_verifyView() {
         mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
 
@@ -223,6 +278,22 @@
     }
 
     @Test
+    public void onBindViewHolder_bindGroupingDevice_verifyView() {
+        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
+        when(mMediaDevice1.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_GROUPING);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        assertThat(mViewHolder.mTitleText.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTitleText.getText().toString()).isEqualTo(TEST_DEVICE_NAME_1);
+        assertThat(mViewHolder.mSeekBar.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mSubTitleText.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mCheckBox.getVisibility()).isEqualTo(View.GONE);
+        assertThat(mViewHolder.mProgressBar.getVisibility()).isEqualTo(View.VISIBLE);
+        assertThat(mViewHolder.mTwoLineLayout.getVisibility()).isEqualTo(View.GONE);
+    }
+
+    @Test
     public void onBindViewHolder_inTransferring_bindNonTransferringDevice_verifyView() {
         when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(true);
         when(mMediaDevice2.getState()).thenReturn(
@@ -256,6 +327,31 @@
     }
 
     @Test
+    public void onItemClick_clicksWithMutingExpectedDeviceExist_cancelsMuteAwaitConnection() {
+        when(mMediaOutputController.isAnyDeviceTransferring()).thenReturn(false);
+        when(mMediaOutputController.hasMutingExpectedDevice()).thenReturn(true);
+        when(mMediaOutputController.isCurrentConnectedDeviceRemote()).thenReturn(false);
+        when(mMediaDevice1.isMutingExpectedDevice()).thenReturn(false);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 0);
+
+        mViewHolder.mContainerLayout.performClick();
+
+        verify(mMediaOutputController).cancelMuteAwaitConnection();
+    }
+
+    @Test
+    public void onItemClick_clicksSelectableDevice_triggerGrouping() {
+        List<MediaDevice> selectableDevices = new ArrayList<>();
+        selectableDevices.add(mMediaDevice2);
+        when(mMediaOutputController.getSelectableMediaDevice()).thenReturn(selectableDevices);
+        mMediaOutputAdapter.onBindViewHolder(mViewHolder, 1);
+
+        mViewHolder.mContainerLayout.performClick();
+
+        verify(mMediaOutputController).addDeviceToPlayMedia(mMediaDevice2);
+    }
+
+    @Test
     public void onItemClick_onGroupActionTriggered_verifySeekbarDisabled() {
         when(mMediaOutputController.getSelectedMediaDevice()).thenReturn(mMediaDevices);
         List<MediaDevice> selectableDevices = new ArrayList<>();
@@ -280,4 +376,14 @@
 
         assertThat(mViewHolder.mSeekBar.isEnabled()).isTrue();
     }
+
+    @Test
+    public void updateColorScheme_triggerController() {
+        WallpaperColors wallpaperColors = WallpaperColors.fromBitmap(
+                Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888));
+
+        mMediaOutputAdapter.updateColorScheme(wallpaperColors, true);
+
+        verify(mMediaOutputController).setCurrentColorScheme(wallpaperColors, true);
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
index eb8ecae..9be201e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputBaseDialogTest.java
@@ -241,46 +241,29 @@
     }
 
     @Test
-    public void onStart_isBroadcasting_verifyRegisterLeBroadcastServiceCallBack() {
+    public void whenBroadcasting_verifyLeBroadcastServiceCallBackIsRegisteredAndUnregistered() {
         when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
                 mLocalBluetoothLeBroadcast);
         mIsBroadcasting = true;
 
         mMediaOutputBaseDialogImpl.onStart();
-
         verify(mLocalBluetoothLeBroadcast).registerServiceCallBack(any(), any());
-    }
-
-    @Test
-    public void onStart_notBroadcasting_noRegisterLeBroadcastServiceCallBack() {
-        when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
-                mLocalBluetoothLeBroadcast);
-        mIsBroadcasting = false;
-
-        mMediaOutputBaseDialogImpl.onStart();
-
-        verify(mLocalBluetoothLeBroadcast, never()).registerServiceCallBack(any(), any());
-    }
-
-    @Test
-    public void onStart_isBroadcasting_verifyUnregisterLeBroadcastServiceCallBack() {
-        when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
-                mLocalBluetoothLeBroadcast);
-        mIsBroadcasting = true;
 
         mMediaOutputBaseDialogImpl.onStop();
-
         verify(mLocalBluetoothLeBroadcast).unregisterServiceCallBack(any());
     }
 
     @Test
-    public void onStop_notBroadcasting_noUnregisterLeBroadcastServiceCallBack() {
+    public void
+            whenNotBroadcasting_verifyLeBroadcastServiceCallBackIsNotRegisteredOrUnregistered() {
         when(mLocalBluetoothProfileManager.getLeAudioBroadcastProfile()).thenReturn(
                 mLocalBluetoothLeBroadcast);
         mIsBroadcasting = false;
 
+        mMediaOutputBaseDialogImpl.onStart();
         mMediaOutputBaseDialogImpl.onStop();
 
+        verify(mLocalBluetoothLeBroadcast, never()).registerServiceCallBack(any(), any());
         verify(mLocalBluetoothLeBroadcast, never()).unregisterServiceCallBack(any());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
index 6dcf802..cb31fde 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputControllerTest.java
@@ -35,6 +35,7 @@
 import android.app.Notification;
 import android.content.Context;
 import android.graphics.drawable.Icon;
+import android.media.AudioDeviceAttributes;
 import android.media.AudioManager;
 import android.media.MediaDescription;
 import android.media.MediaMetadata;
@@ -279,6 +280,203 @@
     }
 
     @Test
+    public void onDeviceListUpdate_isRefreshing_updatesNeedRefreshToTrue() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+        mMediaOutputController.mIsRefreshing = true;
+
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.mNeedRefresh).isTrue();
+    }
+
+    @Test
+    public void cancelMuteAwaitConnection_cancelsWithMediaManager() {
+        when(mAudioManager.getMutingExpectedDevice()).thenReturn(mock(AudioDeviceAttributes.class));
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.cancelMuteAwaitConnection();
+
+        verify(mAudioManager).cancelMuteAwaitConnection(any());
+    }
+
+    @Test
+    public void cancelMuteAwaitConnection_audioManagerIsNull_noAction() {
+        when(mAudioManager.getMutingExpectedDevice()).thenReturn(null);
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+        mMediaOutputController.cancelMuteAwaitConnection();
+
+        verify(mAudioManager, never()).cancelMuteAwaitConnection(any());
+    }
+
+    @Test
+    public void getAppSourceName_packageNameIsNull_returnsNull() {
+        MediaOutputController testMediaOutputController = new MediaOutputController(mSpyContext,
+                "",
+                mMediaSessionManager, mLocalBluetoothManager, mStarter,
+                mNotifCollection, mDialogLaunchAnimator,
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
+        testMediaOutputController.start(mCb);
+        reset(mCb);
+
+        testMediaOutputController.getAppSourceName();
+
+        assertThat(testMediaOutputController.getAppSourceName()).isNull();
+    }
+
+    @Test
+    public void isActiveItem_deviceNotConnected_returnsFalse() {
+        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
+
+        assertThat(mMediaOutputController.isActiveItem(mMediaDevice1)).isFalse();
+    }
+
+    @Test
+    public void getNotificationSmallIcon_packageNameIsNull_returnsNull() {
+        MediaOutputController testMediaOutputController = new MediaOutputController(mSpyContext,
+                "",
+                mMediaSessionManager, mLocalBluetoothManager, mStarter,
+                mNotifCollection, mDialogLaunchAnimator,
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
+        testMediaOutputController.start(mCb);
+        reset(mCb);
+
+        testMediaOutputController.getAppSourceName();
+
+        assertThat(testMediaOutputController.getNotificationSmallIcon()).isNull();
+    }
+
+    @Test
+    public void refreshDataSetIfNeeded_needRefreshIsTrue_setsToFalse() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+        mMediaOutputController.mNeedRefresh = true;
+
+        mMediaOutputController.refreshDataSetIfNeeded();
+
+        assertThat(mMediaOutputController.mNeedRefresh).isFalse();
+    }
+
+    @Test
+    public void isCurrentConnectedDeviceRemote_containsFeatures_returnsTrue() {
+        when(mMediaDevice1.getFeatures()).thenReturn(
+                ImmutableList.of(MediaRoute2Info.FEATURE_REMOTE_PLAYBACK));
+        when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice1);
+
+        assertThat(mMediaOutputController.isCurrentConnectedDeviceRemote()).isTrue();
+    }
+
+    @Test
+    public void addDeviceToPlayMedia_triggersFromLocalMediaManager() {
+        MediaOutputController testMediaOutputController = new MediaOutputController(mSpyContext,
+                null,
+                mMediaSessionManager, mLocalBluetoothManager, mStarter,
+                mNotifCollection, mDialogLaunchAnimator,
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
+
+        LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager);
+        testMediaOutputController.mLocalMediaManager = testLocalMediaManager;
+
+        testMediaOutputController.addDeviceToPlayMedia(mMediaDevice2);
+
+        verify(testLocalMediaManager).addDeviceToPlayMedia(mMediaDevice2);
+    }
+
+    @Test
+    public void removeDeviceFromPlayMedia_triggersFromLocalMediaManager() {
+        MediaOutputController testMediaOutputController = new MediaOutputController(mSpyContext,
+                null,
+                mMediaSessionManager, mLocalBluetoothManager, mStarter,
+                mNotifCollection, mDialogLaunchAnimator,
+                Optional.of(mNearbyMediaDevicesManager), mAudioManager, mPowerExemptionManager,
+                mKeyguardManager);
+
+        LocalMediaManager testLocalMediaManager = spy(testMediaOutputController.mLocalMediaManager);
+        testMediaOutputController.mLocalMediaManager = testLocalMediaManager;
+
+        testMediaOutputController.removeDeviceFromPlayMedia(mMediaDevice2);
+
+        verify(testLocalMediaManager).removeDeviceFromPlayMedia(mMediaDevice2);
+    }
+
+    @Test
+    public void getDeselectableMediaDevice_triggersFromLocalMediaManager() {
+        mMediaOutputController.getDeselectableMediaDevice();
+
+        verify(mLocalMediaManager).getDeselectableMediaDevice();
+    }
+
+    @Test
+    public void adjustSessionVolume_adjustWithoutId_triggersFromLocalMediaManager() {
+        int testVolume = 10;
+        mMediaOutputController.adjustSessionVolume(testVolume);
+
+        verify(mLocalMediaManager).adjustSessionVolume(testVolume);
+    }
+
+    @Test
+    public void getSessionVolumeMax_triggersFromLocalMediaManager() {
+        mMediaOutputController.getSessionVolumeMax();
+
+        verify(mLocalMediaManager).getSessionVolumeMax();
+    }
+
+    @Test
+    public void getSessionVolume_triggersFromLocalMediaManager() {
+        mMediaOutputController.getSessionVolume();
+
+        verify(mLocalMediaManager).getSessionVolume();
+    }
+
+    @Test
+    public void getSessionName_triggersFromLocalMediaManager() {
+        mMediaOutputController.getSessionName();
+
+        verify(mLocalMediaManager).getSessionName();
+    }
+
+    @Test
+    public void releaseSession_triggersFromLocalMediaManager() {
+        mMediaOutputController.releaseSession();
+
+        verify(mLocalMediaManager).releaseSession();
+    }
+
+    @Test
+    public void isAnyDeviceTransferring_noDevicesStateIsConnecting_returnsFalse() {
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.isAnyDeviceTransferring()).isFalse();
+    }
+
+    @Test
+    public void isAnyDeviceTransferring_deviceStateIsConnecting_returnsTrue() {
+        when(mMediaDevice1.getState()).thenReturn(
+                LocalMediaManager.MediaDeviceState.STATE_CONNECTING);
+        mMediaOutputController.start(mCb);
+        reset(mCb);
+
+        mMediaOutputController.onDeviceListUpdate(mMediaDevices);
+
+        assertThat(mMediaOutputController.isAnyDeviceTransferring()).isTrue();
+    }
+
+    @Test
+    public void isPlaying_stateIsNull() {
+        when(mMediaController.getPlaybackState()).thenReturn(null);
+
+        assertThat(mMediaOutputController.isPlaying()).isFalse();
+    }
+
+    @Test
     public void onSelectedDeviceStateChanged_verifyCallback() {
         when(mLocalMediaManager.getCurrentConnectedDevice()).thenReturn(mMediaDevice2);
         mMediaOutputController.start(mCb);
@@ -535,6 +733,44 @@
     }
 
     @Test
+    public void getNotificationSmallIcon_withoutSmallIcon_returnsNull() {
+        final List<NotificationEntry> entryList = new ArrayList<>();
+        final NotificationEntry entry = mock(NotificationEntry.class);
+        final StatusBarNotification sbn = mock(StatusBarNotification.class);
+        final Notification notification = mock(Notification.class);
+        entryList.add(entry);
+
+        when(mNotifCollection.getAllNotifs()).thenReturn(entryList);
+        when(entry.getSbn()).thenReturn(sbn);
+        when(sbn.getNotification()).thenReturn(notification);
+        when(sbn.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
+        when(notification.isMediaNotification()).thenReturn(true);
+        when(notification.getSmallIcon()).thenReturn(null);
+
+        assertThat(mMediaOutputController.getNotificationSmallIcon()).isNull();
+    }
+
+    @Test
+    public void getNotificationSmallIcon_withPackageNameAndMediaSession_returnsIconCompat() {
+        final List<NotificationEntry> entryList = new ArrayList<>();
+        final NotificationEntry entry = mock(NotificationEntry.class);
+        final StatusBarNotification sbn = mock(StatusBarNotification.class);
+        final Notification notification = mock(Notification.class);
+        final Icon icon = mock(Icon.class);
+        entryList.add(entry);
+
+        when(mNotifCollection.getAllNotifs()).thenReturn(entryList);
+        when(entry.getSbn()).thenReturn(sbn);
+        when(sbn.getNotification()).thenReturn(notification);
+        when(sbn.getPackageName()).thenReturn(TEST_PACKAGE_NAME);
+        when(notification.isMediaNotification()).thenReturn(true);
+        when(notification.getSmallIcon()).thenReturn(icon);
+
+        assertThat(mMediaOutputController.getNotificationSmallIcon()).isInstanceOf(
+                IconCompat.class);
+    }
+
+    @Test
     public void isVolumeControlEnabled_isCastWithVolumeFixed_returnsFalse() {
         when(mMediaDevice1.getDeviceType()).thenReturn(
                 MediaDevice.MediaDeviceType.TYPE_CAST_DEVICE);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
index 9557513..bae3569 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dialog/MediaOutputDialogTest.java
@@ -25,7 +25,10 @@
 import static org.mockito.Mockito.when;
 
 import android.app.KeyguardManager;
+import android.graphics.Bitmap;
 import android.media.AudioManager;
+import android.media.MediaDescription;
+import android.media.MediaMetadata;
 import android.media.MediaRoute2Info;
 import android.media.session.MediaController;
 import android.media.session.MediaSessionManager;
@@ -43,6 +46,7 @@
 import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
 import com.android.settingslib.media.LocalMediaManager;
 import com.android.settingslib.media.MediaDevice;
+import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.DialogLaunchAnimator;
 import com.android.systemui.broadcast.BroadcastSender;
@@ -82,6 +86,8 @@
     private final CommonNotifCollection mNotifCollection = mock(CommonNotifCollection.class);
     private final UiEventLogger mUiEventLogger = mock(UiEventLogger.class);
     private final DialogLaunchAnimator mDialogLaunchAnimator = mock(DialogLaunchAnimator.class);
+    private final MediaMetadata mMediaMetadata = mock(MediaMetadata.class);
+    private final MediaDescription  mMediaDescription = mock(MediaDescription.class);
     private final NearbyMediaDevicesManager mNearbyMediaDevicesManager = mock(
             NearbyMediaDevicesManager.class);
     private final AudioManager mAudioManager = mock(AudioManager.class);
@@ -100,6 +106,8 @@
         when(mMediaController.getPlaybackState()).thenReturn(mPlaybackState);
         when(mPlaybackState.getState()).thenReturn(PlaybackState.STATE_NONE);
         when(mMediaController.getPackageName()).thenReturn(TEST_PACKAGE);
+        when(mMediaController.getMetadata()).thenReturn(mMediaMetadata);
+        when(mMediaMetadata.getDescription()).thenReturn(mMediaDescription);
         mMediaControllers.add(mMediaController);
         when(mMediaSessionManager.getActiveSessions(any())).thenReturn(mMediaControllers);
 
@@ -207,6 +215,80 @@
     }
 
     @Test
+    public void getHeaderIcon_getFromMediaControllerMetaData() {
+        int testWidth = 10;
+        int testHeight = 20;
+        when(mMediaDescription.getIconBitmap())
+                .thenReturn(Bitmap.createBitmap(testWidth, testHeight, Bitmap.Config.ARGB_8888));
+
+        assertThat(mMediaOutputDialog.getHeaderIcon().getBitmap().getHeight()).isEqualTo(
+                testHeight);
+        assertThat(mMediaOutputDialog.getHeaderIcon().getBitmap().getWidth()).isEqualTo(testWidth);
+    }
+
+    @Test
+    public void getHeaderText_getFromMediaControllerMetaData() {
+        String testTitle = "test title";
+        when(mMediaDescription.getTitle())
+                .thenReturn(testTitle);
+        assertThat(mMediaOutputDialog.getHeaderText().toString()).isEqualTo(testTitle);
+    }
+
+    @Test
+    public void getHeaderSubtitle_getFromMediaControllerMetaData() {
+        String testSubtitle = "test title";
+        when(mMediaDescription.getSubtitle())
+                .thenReturn(testSubtitle);
+
+        assertThat(mMediaOutputDialog.getHeaderSubtitle().toString()).isEqualTo(testSubtitle);
+    }
+
+    @Test
+    public void getStopButtonText_notSupportsBroadcast_returnsDefaultText() {
+        String stopText = mContext.getText(R.string.keyboard_key_media_stop).toString();
+        MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
+        when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false);
+
+        MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
+                mockMediaOutputController, mUiEventLogger);
+        testDialog.show();
+
+        assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
+    }
+
+    @Test
+    public void getStopButtonText_supportsBroadcast_returnsBroadcastText() {
+        String stopText = mContext.getText(R.string.media_output_broadcast).toString();
+        MediaDevice mMediaDevice = mock(MediaDevice.class);
+        MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
+        when(mockMediaOutputController.isBroadcastSupported()).thenReturn(true);
+        when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(mMediaDevice);
+        when(mockMediaOutputController.isBluetoothLeDevice(any())).thenReturn(true);
+        when(mockMediaOutputController.isPlaying()).thenReturn(true);
+        when(mockMediaOutputController.isBluetoothLeBroadcastEnabled()).thenReturn(false);
+        MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
+                mockMediaOutputController, mUiEventLogger);
+        testDialog.show();
+
+        assertThat(testDialog.getStopButtonText().toString()).isEqualTo(stopText);
+    }
+
+    @Test
+    public void onStopButtonClick_notPlaying_releaseSession() {
+        MediaOutputController mockMediaOutputController = mock(MediaOutputController.class);
+        when(mockMediaOutputController.isBroadcastSupported()).thenReturn(false);
+        when(mockMediaOutputController.getCurrentConnectedMediaDevice()).thenReturn(null);
+        when(mockMediaOutputController.isPlaying()).thenReturn(false);
+        MediaOutputDialog testDialog = new MediaOutputDialog(mContext, false, mBroadcastSender,
+                mockMediaOutputController, mUiEventLogger);
+        testDialog.show();
+
+        testDialog.onStopButtonClick();
+
+        verify(mockMediaOutputController).releaseSession();
+    }
+
+    @Test
     // Check the visibility metric logging by creating a new MediaOutput dialog,
     // and verify if the calling times increases.
     public void onCreate_ShouldLogVisibility() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
index 0bfc034..2f52950 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/dream/MediaDreamSentinelTest.java
@@ -16,7 +16,7 @@
 
 package com.android.systemui.media.dream;
 
-import static com.android.systemui.flags.Flags.MEDIA_DREAM_COMPLICATION;
+import static com.android.systemui.flags.Flags.DREAM_MEDIA_COMPLICATION;
 
 import static org.mockito.AdditionalMatchers.not;
 import static org.mockito.ArgumentMatchers.any;
@@ -68,7 +68,7 @@
     public void setup() {
         MockitoAnnotations.initMocks(this);
 
-        when(mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)).thenReturn(true);
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_COMPLICATION)).thenReturn(true);
     }
 
     @Test
@@ -137,7 +137,7 @@
 
     @Test
     public void testOnMediaDataLoaded_mediaComplicationDisabled_doesNotAddComplication() {
-        when(mFeatureFlags.isEnabled(MEDIA_DREAM_COMPLICATION)).thenReturn(false);
+        when(mFeatureFlags.isEnabled(DREAM_MEDIA_COMPLICATION)).thenReturn(false);
 
         final MediaDreamSentinel sentinel = new MediaDreamSentinel(mContext, mMediaDataManager,
                 mDreamOverlayStateController, mMediaEntryComplication, mFeatureFlags);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
new file mode 100644
index 0000000..37b7f47
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
@@ -0,0 +1,119 @@
+package com.android.systemui.mediaprojection.appselector
+
+import android.content.ComponentName
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.mediaprojection.appselector.data.RecentTask
+import com.android.systemui.mediaprojection.appselector.data.RecentTaskListProvider
+import com.android.systemui.util.mockito.mock
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class MediaProjectionAppSelectorControllerTest : SysuiTestCase() {
+
+    private val taskListProvider = TestRecentTaskListProvider()
+    private val scope = CoroutineScope(Dispatchers.Unconfined)
+    private val appSelectorComponentName = ComponentName("com.test", "AppSelector")
+
+    private val view: MediaProjectionAppSelectorView = mock()
+
+    private val controller = MediaProjectionAppSelectorController(
+        taskListProvider,
+        scope,
+        appSelectorComponentName
+    )
+
+    @Test
+    fun initNoRecentTasks_bindsEmptyList() {
+        taskListProvider.tasks = emptyList()
+
+        controller.init(view)
+
+        verify(view).bind(emptyList())
+    }
+
+    @Test
+    fun initOneRecentTask_bindsList() {
+        taskListProvider.tasks = listOf(
+            createRecentTask(taskId = 1)
+        )
+
+        controller.init(view)
+
+        verify(view).bind(
+            listOf(
+                createRecentTask(taskId = 1)
+            )
+        )
+    }
+
+    @Test
+    fun initMultipleRecentTasksWithoutAppSelectorTask_bindsListInReverse() {
+        val tasks = listOf(
+            createRecentTask(taskId = 1),
+            createRecentTask(taskId = 2),
+            createRecentTask(taskId = 3),
+        )
+        taskListProvider.tasks = tasks
+
+        controller.init(view)
+
+        verify(view).bind(
+            listOf(
+                createRecentTask(taskId = 3),
+                createRecentTask(taskId = 2),
+                createRecentTask(taskId = 1),
+            )
+        )
+    }
+
+    @Test
+    fun initRecentTasksWithAppSelectorTasks_bindsListInReverseAndAppSelectorTasksAtTheEnd() {
+        val tasks = listOf(
+            createRecentTask(taskId = 1),
+            createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName),
+            createRecentTask(taskId = 3),
+            createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName),
+            createRecentTask(taskId = 5),
+        )
+        taskListProvider.tasks = tasks
+
+        controller.init(view)
+
+        verify(view).bind(
+            listOf(
+                createRecentTask(taskId = 5),
+                createRecentTask(taskId = 3),
+                createRecentTask(taskId = 1),
+                createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName),
+                createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName),
+            )
+        )
+    }
+
+    private fun createRecentTask(
+        taskId: Int,
+        topActivityComponent: ComponentName? = null
+    ): RecentTask {
+        return RecentTask(
+            taskId = taskId,
+            topActivityComponent = topActivityComponent,
+            baseIntentComponent = ComponentName("com", "Test"),
+            userId = 0
+        )
+    }
+
+    private class TestRecentTaskListProvider : RecentTaskListProvider {
+
+        var tasks: List<RecentTask> = emptyList()
+
+        override suspend fun loadRecentTasks(): List<RecentTask> = tasks
+
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
index ecc8457..cbe1186 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java
@@ -226,7 +226,9 @@
                 + "  Tile records:\n"
                 + "    " + mockTileString + "\n"
                 + "    " + mockTileViewString + "\n"
-                + "  media bounds: null\n";
+                + "  media bounds: null\n"
+                + "  horizontal layout: false\n"
+                + "  last orientation: 0\n";
         assertEquals(expected, w.getBuffer().toString());
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
index 39f27d4..4af5b90 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt
@@ -123,8 +123,7 @@
 
     @Test
     fun mediaExpansion_afterConfigChange_inLandscape_collapsedInLandscapeTrue_updatesToCollapsed() {
-        // times(2) because both controller and base controller are registering their listeners
-        verify(quickQSPanel, times(2)).addOnConfigurationChangedListener(captor.capture())
+        verify(quickQSPanel).addOnConfigurationChangedListener(captor.capture())
 
         // verify that media starts in the expanded state by default
         verify(mediaHost).expansion = MediaHostState.EXPANDED
@@ -139,8 +138,7 @@
 
     @Test
     fun mediaExpansion_afterConfigChange_landscape_collapsedInLandscapeFalse_remainsExpanded() {
-        // times(2) because both controller and base controller are registering their listeners
-        verify(quickQSPanel, times(2)).addOnConfigurationChangedListener(captor.capture())
+        verify(quickQSPanel).addOnConfigurationChangedListener(captor.capture())
         reset(mediaHost)
 
         usingCollapsedLandscapeMedia = false
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
index 2adc389..481e4e9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt
@@ -21,12 +21,16 @@
 import android.view.MotionEvent
 import android.view.ViewGroup
 import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardHostViewController
 import com.android.keyguard.LockIconViewController
+import com.android.keyguard.dagger.KeyguardBouncerComponent
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollectorFake
 import com.android.systemui.dock.DockManager
+import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel
 import com.android.systemui.shade.NotificationShadeWindowView.InteractionEventHandler
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
 import com.android.systemui.statusbar.NotificationShadeDepthController
@@ -51,9 +55,9 @@
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
+@SmallTest
 @RunWith(AndroidTestingRunner::class)
 @RunWithLooper(setAsMainLooper = true)
-@SmallTest
 class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
     @Mock
     private lateinit var view: NotificationShadeWindowView
@@ -72,8 +76,12 @@
     @Mock
     private lateinit var keyguardUnlockAnimationController: KeyguardUnlockAnimationController
     @Mock
+    private lateinit var featureFlags: FeatureFlags
+    @Mock
     private lateinit var ambientState: AmbientState
     @Mock
+    private lateinit var keyguardBouncerViewModel: KeyguardBouncerViewModel
+    @Mock
     private lateinit var stackScrollLayoutController: NotificationStackScrollLayoutController
     @Mock
     private lateinit var statusBarKeyguardViewManager: StatusBarKeyguardViewManager
@@ -87,6 +95,10 @@
     private lateinit var phoneStatusBarViewController: PhoneStatusBarViewController
     @Mock
     private lateinit var pulsingGestureListener: PulsingGestureListener
+    @Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
+    @Mock lateinit var keyguardBouncerContainer: ViewGroup
+    @Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
+    @Mock lateinit var keyguardHostViewController: KeyguardHostViewController
 
     private lateinit var interactionEventHandlerCaptor: ArgumentCaptor<InteractionEventHandler>
     private lateinit var interactionEventHandler: InteractionEventHandler
@@ -97,7 +109,6 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
         whenever(view.bottom).thenReturn(VIEW_BOTTOM)
-
         underTest = NotificationShadeWindowViewController(
             lockscreenShadeTransitionController,
             FalsingCollectorFake(),
@@ -115,7 +126,10 @@
             notificationShadeWindowController,
             keyguardUnlockAnimationController,
             ambientState,
-            pulsingGestureListener
+            pulsingGestureListener,
+            featureFlags,
+            keyguardBouncerViewModel,
+            keyguardBouncerComponentFactory
         )
         underTest.setupExpandedStatusBar()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
index 001bfee..4a7dec9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
@@ -33,11 +33,14 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.keyguard.LockIconViewController;
+import com.android.keyguard.dagger.KeyguardBouncerComponent;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.classifier.FalsingCollectorFake;
 import com.android.systemui.dock.DockManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardBouncerViewModel;
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.NotificationShadeDepthController;
@@ -86,6 +89,9 @@
     @Mock private KeyguardUnlockAnimationController mKeyguardUnlockAnimationController;
     @Mock private AmbientState mAmbientState;
     @Mock private PulsingGestureListener mPulsingGestureListener;
+    @Mock private FeatureFlags mFeatureFlags;
+    @Mock private KeyguardBouncerViewModel mKeyguardBouncerViewModel;
+    @Mock private KeyguardBouncerComponent.Factory mKeyguardBouncerComponentFactory;
 
     @Captor private ArgumentCaptor<NotificationShadeWindowView.InteractionEventHandler>
             mInteractionEventHandlerCaptor;
@@ -121,7 +127,10 @@
                 mNotificationShadeWindowController,
                 mKeyguardUnlockAnimationController,
                 mAmbientState,
-                mPulsingGestureListener
+                mPulsingGestureListener,
+                mFeatureFlags,
+                mKeyguardBouncerViewModel,
+                mKeyguardBouncerComponentFactory
         );
         mController.setupExpandedStatusBar();
         mController.setDragDownHelper(mDragDownHelper);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 945cf7f..9a13e93 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -635,6 +635,19 @@
     }
 
     @Test
+    public void transientIndication_visibleWhenDozing_ignoresPowerPressed() {
+        createController();
+
+        mController.setVisible(true);
+        reset(mRotateTextViewController);
+        mController.getKeyguardCallback().onBiometricError(
+                FingerprintManager.BIOMETRIC_ERROR_POWER_PRESSED, "foo",
+                BiometricSourceType.FINGERPRINT);
+
+        verifyNoMessage(INDICATION_TYPE_BIOMETRIC_MESSAGE);
+    }
+
+    @Test
     public void transientIndication_swipeUpToRetry() {
         createController();
         String message = mContext.getString(R.string.keyguard_retry);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index a4453f8..ee4b9d9c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -41,11 +41,17 @@
 import com.android.internal.widget.LockPatternUtils;
 import com.android.keyguard.KeyguardMessageArea;
 import com.android.keyguard.KeyguardMessageAreaController;
+import com.android.keyguard.KeyguardSecurityModel;
 import com.android.keyguard.KeyguardUpdateMonitor;
 import com.android.keyguard.ViewMediatorCallback;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dock.DockManager;
 import com.android.systemui.dreams.DreamOverlayStateController;
+import com.android.systemui.flags.FeatureFlags;
+import com.android.systemui.keyguard.data.BouncerView;
+import com.android.systemui.keyguard.data.BouncerViewDelegate;
+import com.android.systemui.keyguard.domain.interactor.BouncerCallbackInteractor;
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor;
 import com.android.systemui.navigationbar.NavigationModeController;
 import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
 import com.android.systemui.shade.NotificationPanelViewController;
@@ -101,6 +107,13 @@
     @Mock private SysUIUnfoldComponent mSysUiUnfoldComponent;
     @Mock private DreamOverlayStateController mDreamOverlayStateController;
     @Mock private LatencyTracker mLatencyTracker;
+    @Mock private FeatureFlags mFeatureFlags;
+    @Mock private KeyguardSecurityModel mKeyguardSecurityModel;
+    @Mock private BouncerCallbackInteractor mBouncerCallbackInteractor;
+    @Mock private BouncerInteractor mBouncerInteractor;
+    @Mock private BouncerView mBouncerView;
+//    @Mock private WeakReference<BouncerViewDelegate> mBouncerViewDelegateWeakReference;
+    @Mock private BouncerViewDelegate mBouncerViewDelegate;
 
     private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private KeyguardBouncer.BouncerExpansionCallback mBouncerExpansionCallback;
@@ -115,6 +128,8 @@
         when(mContainer.findViewById(anyInt())).thenReturn(mKeyguardMessageArea);
         when(mKeyguardMessageAreaFactory.create(any(KeyguardMessageArea.class)))
                 .thenReturn(mKeyguardMessageAreaController);
+        when(mBouncerView.getDelegate()).thenReturn(mBouncerViewDelegate);
+
         mStatusBarKeyguardViewManager =
                 new StatusBarKeyguardViewManager(
                         getContext(),
@@ -133,7 +148,12 @@
                         mKeyguardMessageAreaFactory,
                         Optional.of(mSysUiUnfoldComponent),
                         () -> mShadeController,
-                        mLatencyTracker);
+                        mLatencyTracker,
+                        mKeyguardSecurityModel,
+                        mFeatureFlags,
+                        mBouncerCallbackInteractor,
+                        mBouncerInteractor,
+                        mBouncerView);
         mStatusBarKeyguardViewManager.registerCentralSurfaces(
                 mCentralSurfaces,
                 mNotificationPanelView,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
similarity index 97%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
index 37c0f36..bf43238 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/userswitcher/StatusBarUserSwitcherControllerOldImplTest.kt
@@ -34,14 +34,14 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper
 @SmallTest
-class StatusBarUserSwitcherControllerTest : SysuiTestCase() {
+class StatusBarUserSwitcherControllerOldImplTest : SysuiTestCase() {
     @Mock
     private lateinit var tracker: StatusBarUserInfoTracker
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt
new file mode 100644
index 0000000..f304647
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BaseUserSwitcherAdapterTest.kt
@@ -0,0 +1,264 @@
+/*
+ * 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.policy
+
+import android.content.pm.UserInfo
+import android.graphics.Bitmap
+import android.os.UserHandle
+import android.view.View
+import android.view.ViewGroup
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.qs.user.UserSwitchDialogController
+import com.android.systemui.user.data.source.UserRecord
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.mock
+import com.google.common.truth.Truth.assertThat
+import java.lang.ref.WeakReference
+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 BaseUserSwitcherAdapterTest : SysuiTestCase() {
+
+    @Mock private lateinit var controller: UserSwitcherController
+
+    private lateinit var underTest: BaseUserSwitcherAdapter
+
+    private lateinit var users: ArrayList<UserRecord>
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        users =
+            ArrayList(
+                listOf(
+                    createUserRecord(
+                        id = 0,
+                        picture = mock(),
+                        isSelected = true,
+                        isGuest = false,
+                    ),
+                    createUserRecord(
+                        id = 1,
+                        picture = mock(),
+                        isSelected = false,
+                        isGuest = false,
+                    ),
+                    createUserRecord(
+                        id = UserHandle.USER_NULL,
+                        picture = null,
+                        isSelected = false,
+                        isGuest = true,
+                    ),
+                )
+            )
+
+        whenever(controller.users).thenAnswer { users }
+
+        underTest =
+            object : BaseUserSwitcherAdapter(controller) {
+                override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
+                    return mock()
+                }
+            }
+    }
+
+    @Test
+    fun `Adds self to controller in constructor`() {
+        val captor = kotlinArgumentCaptor<WeakReference<BaseUserSwitcherAdapter>>()
+        verify(controller).addAdapter(captor.capture())
+
+        assertThat(captor.value.get()).isEqualTo(underTest)
+    }
+
+    @Test
+    fun count() {
+        assertThat(underTest.count).isEqualTo(users.size)
+    }
+
+    @Test
+    fun `count - ignores restricted users when device is locked`() {
+        whenever(controller.isKeyguardShowing).thenReturn(true)
+        users =
+            ArrayList(
+                listOf(
+                    createUserRecord(
+                        id = 0,
+                        picture = mock(),
+                        isSelected = true,
+                        isGuest = false,
+                        isRestricted = false,
+                    ),
+                    createUserRecord(
+                        id = 1,
+                        picture = mock(),
+                        isSelected = false,
+                        isGuest = false,
+                        isRestricted = true, // this one will be ignored.
+                    ),
+                    createUserRecord(
+                        id = UserHandle.USER_NULL,
+                        picture = null,
+                        isSelected = false,
+                        isGuest = true,
+                    ),
+                )
+            )
+        assertThat(underTest.count).isEqualTo(users.size - 1)
+    }
+
+    @Test
+    fun `count - does not ignore restricted users when device is not locked`() {
+        whenever(controller.isKeyguardShowing).thenReturn(false)
+        users =
+            ArrayList(
+                listOf(
+                    createUserRecord(
+                        id = 0,
+                        picture = mock(),
+                        isSelected = true,
+                        isGuest = false,
+                        isRestricted = false,
+                    ),
+                    createUserRecord(
+                        id = 1,
+                        picture = mock(),
+                        isSelected = false,
+                        isGuest = false,
+                        isRestricted = true,
+                    ),
+                    createUserRecord(
+                        id = UserHandle.USER_NULL,
+                        picture = null,
+                        isSelected = false,
+                        isGuest = true,
+                    ),
+                )
+            )
+        assertThat(underTest.count).isEqualTo(users.size)
+    }
+
+    @Test
+    fun getItem() {
+        assertThat((0 until underTest.count).map { position -> underTest.getItem(position) })
+            .isEqualTo(users)
+    }
+
+    @Test
+    fun getItemId() {
+        (0 until underTest.count).map { position ->
+            assertThat(underTest.getItemId(position)).isEqualTo(position)
+        }
+    }
+
+    @Test
+    fun onUserListItemClicked() {
+        val userRecord = users[users.size / 2]
+        val dialogShower: UserSwitchDialogController.DialogShower = mock()
+
+        underTest.onUserListItemClicked(userRecord, dialogShower)
+
+        verify(controller).onUserListItemClicked(userRecord, dialogShower)
+    }
+
+    @Test
+    fun `getName - non guest - returns real name`() {
+        val userRecord =
+            createUserRecord(
+                id = 1,
+                picture = mock(),
+            )
+
+        assertThat(underTest.getName(context, userRecord)).isEqualTo(userRecord.info?.name)
+    }
+
+    @Test
+    fun `getName - guest and selected - returns exit guest action name`() {
+        val expected = "Exit guest"
+        context.orCreateTestableResources.addOverride(
+            com.android.settingslib.R.string.guest_exit_quick_settings_button,
+            expected,
+        )
+
+        val userRecord =
+            createUserRecord(
+                id = 2,
+                picture = null,
+                isGuest = true,
+                isSelected = true,
+            )
+
+        assertThat(underTest.getName(context, userRecord)).isEqualTo(expected)
+    }
+
+    @Test
+    fun `getName - guest and not selected - returns enter guest action name`() {
+        val expected = "Guest"
+        context.orCreateTestableResources.addOverride(
+            com.android.internal.R.string.guest_name,
+            expected,
+        )
+
+        val userRecord =
+            createUserRecord(
+                id = 2,
+                picture = null,
+                isGuest = true,
+                isSelected = false,
+            )
+
+        assertThat(underTest.getName(context, userRecord)).isEqualTo("Guest")
+    }
+
+    @Test
+    fun refresh() {
+        underTest.refresh()
+
+        verify(controller).refreshUsers(UserHandle.USER_NULL)
+    }
+
+    private fun createUserRecord(
+        id: Int,
+        picture: Bitmap? = null,
+        isSelected: Boolean = false,
+        isGuest: Boolean = false,
+        isAction: Boolean = false,
+        isRestricted: Boolean = false,
+    ): UserRecord {
+        return UserRecord(
+            info =
+                if (isAction) {
+                    null
+                } else {
+                    UserInfo(id, "name$id", 0)
+                },
+            picture = picture,
+            isCurrent = isSelected,
+            isGuest = isGuest,
+            isRestricted = isRestricted,
+        )
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
index fda80a2..43d0fe9 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BatteryControllerTest.java
@@ -42,6 +42,7 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.broadcast.BroadcastDispatcher;
 import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.power.EnhancedEstimates;
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback;
 
@@ -80,6 +81,7 @@
                 mPowerManager,
                 mBroadcastDispatcher,
                 mDemoModeController,
+                mock(DumpManager.class),
                 new Handler(),
                 new Handler());
         // Can throw if updateEstimate is called on the main thread
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchControllerTest.kt
index b4f3987b..b86ca6f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/KeyguardQsUserSwitchControllerTest.kt
@@ -38,9 +38,9 @@
 import org.junit.Test
 import org.junit.runner.RunWith
 import org.mockito.Mock
-import org.mockito.Mockito.`when`
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
+import org.mockito.Mockito.`when`
 import org.mockito.MockitoAnnotations
 
 @SmallTest
@@ -102,8 +102,7 @@
 
         ViewUtils.attachView(view)
         testableLooper.processAllMessages()
-        `when`(userSwitcherController.keyguardStateController).thenReturn(keyguardStateController)
-        `when`(userSwitcherController.keyguardStateController.isShowing).thenReturn(true)
+        `when`(userSwitcherController.isKeyguardShowing).thenReturn(true)
         `when`(keyguardStateController.isShowing).thenReturn(true)
         `when`(keyguardStateController.isKeyguardGoingAway).thenReturn(false)
         keyguardQsUserSwitchController.init()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
similarity index 98%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
index 8dcd4bb..76ecc1c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/UserSwitcherControllerOldImplTest.kt
@@ -86,7 +86,7 @@
 @RunWith(AndroidTestingRunner::class)
 @TestableLooper.RunWithLooper(setAsMainLooper = true)
 @SmallTest
-class UserSwitcherControllerTest : SysuiTestCase() {
+class UserSwitcherControllerOldImplTest : SysuiTestCase() {
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var activityManager: IActivityManager
     @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
@@ -118,7 +118,7 @@
     private lateinit var longRunningExecutor: FakeExecutor
     private lateinit var uiExecutor: FakeExecutor
     private lateinit var uiEventLogger: UiEventLoggerFake
-    private lateinit var userSwitcherController: UserSwitcherController
+    private lateinit var userSwitcherController: UserSwitcherControllerOldImpl
     private lateinit var picture: Bitmap
     private val ownerId = UserHandle.USER_SYSTEM
     private val ownerInfo = UserInfo(ownerId, "Owner", null,
@@ -205,7 +205,8 @@
     }
 
     private fun setupController() {
-        userSwitcherController = UserSwitcherController(
+        userSwitcherController =
+            UserSwitcherControllerOldImpl(
                 mContext,
                 activityManager,
                 userManager,
@@ -230,7 +231,8 @@
                 dumpManager,
                 dialogLaunchAnimator,
                 guestResumeSessionReceiver,
-                guestResetOrExitSessionReceiver)
+                guestResetOrExitSessionReceiver
+            )
         userSwitcherController.init(notificationShadeWindowView)
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
index 6b466e1..6fec343 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt
@@ -60,7 +60,7 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        whenever(controller.addUsersFromLockScreen).thenReturn(MutableStateFlow(false))
+        whenever(controller.isAddUsersFromLockScreenEnabled).thenReturn(MutableStateFlow(false))
         whenever(controller.isGuestUserAutoCreated).thenReturn(false)
         whenever(controller.isGuestUserResetting).thenReturn(false)
 
diff --git a/services/Android.bp b/services/Android.bp
index 2756e6e..ccdf7ca 100644
--- a/services/Android.bp
+++ b/services/Android.bp
@@ -101,7 +101,6 @@
         ":services.profcollect-sources",
         ":services.restrictions-sources",
         ":services.searchui-sources",
-        ":services.selectiontoolbar-sources",
         ":services.smartspace-sources",
         ":services.speech-sources",
         ":services.systemcaptions-sources",
@@ -156,7 +155,6 @@
         "services.profcollect",
         "services.restrictions",
         "services.searchui",
-        "services.selectiontoolbar",
         "services.smartspace",
         "services.speech",
         "services.systemcaptions",
diff --git a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
index b5fdaca..6bb19ce 100644
--- a/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
+++ b/services/autofill/java/com/android/server/autofill/PresentationStatsEventLogger.java
@@ -32,6 +32,8 @@
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__UNKNOWN_AUTOFILL_DISPLAY_PRESENTATION_TYPE;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_ACTIVITY_FINISHED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_FILL_REQUEST_FAILED;
+import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_NO_FOCUS;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_REQUEST_TIMEOUT;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_SESSION_COMMITTED_PREMATURELY;
 import static com.android.internal.util.FrameworkStatsLog.AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_UNKNOWN_REASON;
@@ -72,19 +74,32 @@
             NOT_SHOWN_REASON_VIEW_CHANGED,
             NOT_SHOWN_REASON_ACTIVITY_FINISHED,
             NOT_SHOWN_REASON_REQUEST_TIMEOUT,
+            NOT_SHOWN_REASON_REQUEST_FAILED,
+            NOT_SHOWN_REASON_NO_FOCUS,
             NOT_SHOWN_REASON_SESSION_COMMITTED_PREMATURELY,
             NOT_SHOWN_REASON_UNKNOWN
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface NotShownReason {}
 
-    public static final int NOT_SHOWN_REASON_ANY_SHOWN = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
-    public static final int NOT_SHOWN_REASON_VIEW_FOCUS_CHANGED = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_VIEW_FOCUS_CHANGED;
-    public static final int NOT_SHOWN_REASON_VIEW_CHANGED = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_VIEW_CHANGED;
-    public static final int NOT_SHOWN_REASON_ACTIVITY_FINISHED = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_ACTIVITY_FINISHED;
-    public static final int NOT_SHOWN_REASON_REQUEST_TIMEOUT = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_REQUEST_TIMEOUT;
-    public static final int NOT_SHOWN_REASON_SESSION_COMMITTED_PREMATURELY = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_SESSION_COMMITTED_PREMATURELY;
-    public static final int NOT_SHOWN_REASON_UNKNOWN = AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_UNKNOWN_REASON;
+    public static final int NOT_SHOWN_REASON_ANY_SHOWN =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__ANY_SHOWN;
+    public static final int NOT_SHOWN_REASON_VIEW_FOCUS_CHANGED =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_VIEW_FOCUS_CHANGED;
+    public static final int NOT_SHOWN_REASON_VIEW_CHANGED =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_VIEW_CHANGED;
+    public static final int NOT_SHOWN_REASON_ACTIVITY_FINISHED =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_ACTIVITY_FINISHED;
+    public static final int NOT_SHOWN_REASON_REQUEST_TIMEOUT =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_REQUEST_TIMEOUT;
+    public static final int NOT_SHOWN_REASON_REQUEST_FAILED =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_FILL_REQUEST_FAILED;
+    public static final int NOT_SHOWN_REASON_NO_FOCUS =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_NO_FOCUS;
+    public static final int NOT_SHOWN_REASON_SESSION_COMMITTED_PREMATURELY =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_SESSION_COMMITTED_PREMATURELY;
+    public static final int NOT_SHOWN_REASON_UNKNOWN =
+            AUTOFILL_PRESENTATION_EVENT_REPORTED__PRESENTATION_EVENT_RESULT__NONE_SHOWN_UNKNOWN_REASON;
 
     private final int mSessionId;
     private Optional<PresentationStatsEventInternal> mEventInternal;
@@ -118,6 +133,14 @@
         });
     }
 
+    public void maybeSetNoPresentationEventReasonIfNoReasonExists(@NotShownReason int reason) {
+        mEventInternal.ifPresent(event -> {
+            if (event.mCountShown == 0 && event.mNoPresentationReason == NOT_SHOWN_REASON_UNKNOWN) {
+                event.mNoPresentationReason = reason;
+            }
+        });
+    }
+
     public void maybeSetAvailableCount(@Nullable List<Dataset> datasetList,
             AutofillId currentViewId) {
         mEventInternal.ifPresent(event -> {
@@ -180,7 +203,8 @@
 
     public void maybeSetInlinePresentationAndSuggestionHostUid(Context context, int userId) {
         mEventInternal.ifPresent(event -> {
-            event.mDisplayPresentationType = UI_TYPE_INLINE;
+            event.mDisplayPresentationType =
+                AUTOFILL_PRESENTATION_EVENT_REPORTED__DISPLAY_PRESENTATION_TYPE__INLINE;
             String imeString = Settings.Secure.getStringForUser(context.getContentResolver(),
                     Settings.Secure.DEFAULT_INPUT_METHOD, userId);
             if (TextUtils.isEmpty(imeString)) {
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 3d955b7..5c11e2c 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -43,6 +43,8 @@
 import static com.android.server.autofill.Helper.sDebug;
 import static com.android.server.autofill.Helper.sVerbose;
 import static com.android.server.autofill.Helper.toArray;
+import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_NO_FOCUS;
+import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_REQUEST_FAILED;
 import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_REQUEST_TIMEOUT;
 import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_VIEW_CHANGED;
 import static com.android.server.autofill.PresentationStatsEventLogger.NOT_SHOWN_REASON_VIEW_FOCUS_CHANGED;
@@ -402,6 +404,13 @@
     @GuardedBy("mLock")
     private PresentationStatsEventLogger mPresentationStatsEventLogger;
 
+    /**
+     * Fill dialog request would likely be sent slightly later.
+     */
+    @NonNull
+    @GuardedBy("mLock")
+    private boolean mStartedLogEventWithoutFocus;
+
     void onSwitchInputMethodLocked() {
         // One caveat is that for the case where the focus is on a field for which regular autofill
         // returns null, and augmented autofill is triggered,  and then the user switches the input
@@ -522,6 +531,7 @@
             }
             mLastFillRequest = mPendingFillRequest;
 
+            mPresentationStatsEventLogger.maybeSetIsNewRequest(true);
             mRemoteFillService.onFillRequest(mPendingFillRequest);
             mPendingInlineSuggestionsRequest = null;
             mWaitForInlineRequest = false;
@@ -1128,6 +1138,7 @@
     @Override
     public void onFillRequestSuccess(int requestId, @Nullable FillResponse response,
             @NonNull String servicePackageName, int requestFlags) {
+
         final AutofillId[] fieldClassificationIds;
 
         final LogMaker requestLog;
@@ -1285,9 +1296,8 @@
                 }
             }
 
-            // TODO(b/234185326): Add separate reason for failures.
             mPresentationStatsEventLogger.maybeSetNoPresentationEventReason(
-                    NOT_SHOWN_REASON_REQUEST_TIMEOUT);
+                    timedOut ? NOT_SHOWN_REASON_REQUEST_TIMEOUT : NOT_SHOWN_REASON_REQUEST_FAILED);
             mPresentationStatsEventLogger.logAndEndEvent();
         }
         notifyUnavailableToClient(AutofillManager.STATE_UNKNOWN_FAILED,
@@ -2816,6 +2826,7 @@
     @GuardedBy("mLock")
     private boolean requestNewFillResponseOnViewEnteredIfNecessaryLocked(@NonNull AutofillId id,
             @NonNull ViewState viewState, int flags) {
+        // Force new response for manual request
         if ((flags & FLAG_MANUAL_REQUEST) != 0) {
             mSessionFlags.mAugmentedAutofillOnly = false;
             if (sDebug) Slog.d(TAG, "Re-starting session on view " + id + " and flags " + flags);
@@ -2823,7 +2834,7 @@
             return true;
         }
 
-        // If it's not, then check if it it should start a partition.
+        // If it's not, then check if it should start a partition.
         if (shouldStartNewPartitionLocked(id)) {
             if (sDebug) {
                 Slog.d(TAG, "Starting partition or augmented request for view id " + id + ": "
@@ -2922,7 +2933,7 @@
             if (sDebug) {
                 Slog.d(TAG, "Set the response has expired.");
             }
-            mPresentationStatsEventLogger.maybeSetNoPresentationEventReason(
+            mPresentationStatsEventLogger.maybeSetNoPresentationEventReasonIfNoReasonExists(
                         NOT_SHOWN_REASON_VIEW_CHANGED);
             mPresentationStatsEventLogger.logAndEndEvent();
             return;
@@ -2967,11 +2978,19 @@
                 // View is triggering autofill.
                 mCurrentViewId = viewState.id;
                 viewState.update(value, virtualBounds, flags);
-                if (!isRequestSupportFillDialog(flags)) {
-                    mSessionFlags.mFillDialogDisabled = true;
-                }
                 mPresentationStatsEventLogger.startNewEvent();
                 mPresentationStatsEventLogger.maybeSetAutofillServiceUid(getAutofillServiceUid());
+                if (isRequestSupportFillDialog(flags)) {
+                    // Set the default reason for now if the user doesn't trigger any focus event
+                    // on the autofillable view. This can be changed downstream when more
+                    // information is available or session is committed.
+                    mPresentationStatsEventLogger.maybeSetNoPresentationEventReason(
+                            NOT_SHOWN_REASON_NO_FOCUS);
+                    mStartedLogEventWithoutFocus = true;
+                } else {
+                    mSessionFlags.mFillDialogDisabled = true;
+                    mStartedLogEventWithoutFocus = false;
+                }
                 requestNewFillResponseLocked(viewState, ViewState.STATE_STARTED_SESSION, flags);
                 break;
             case ACTION_VALUE_CHANGED:
@@ -3014,6 +3033,8 @@
                 }
                 break;
             case ACTION_VIEW_ENTERED:
+                boolean startedEventWithoutFocus = mStartedLogEventWithoutFocus;
+                mStartedLogEventWithoutFocus = false;
                 if (sVerbose && virtualBounds != null) {
                     Slog.v(TAG, "entered on virtual child " + id + ": " + virtualBounds);
                 }
@@ -3030,9 +3051,15 @@
                     return;
                 }
 
-                mPresentationStatsEventLogger.maybeSetNoPresentationEventReason(
-                        NOT_SHOWN_REASON_VIEW_FOCUS_CHANGED);
-                mPresentationStatsEventLogger.logAndEndEvent();
+                // Previously, fill request will only start whenever a view is entered.
+                // With Fill Dialog, request starts prior to view getting entered. So, we can't end
+                // the event at this moment, otherwise we will be wrongly attributing fill dialog
+                // event as concluded.
+                if (!startedEventWithoutFocus) {
+                    mPresentationStatsEventLogger.maybeSetNoPresentationEventReason(
+                            NOT_SHOWN_REASON_VIEW_FOCUS_CHANGED);
+                    mPresentationStatsEventLogger.logAndEndEvent();
+                }
 
                 if ((flags & FLAG_MANUAL_REQUEST) == 0) {
                     // Not a manual request
@@ -3060,9 +3087,22 @@
                     }
                 }
 
-                mPresentationStatsEventLogger.startNewEvent();
-                mPresentationStatsEventLogger.maybeSetAutofillServiceUid(getAutofillServiceUid());
+                if (!startedEventWithoutFocus) {
+                    mPresentationStatsEventLogger.startNewEvent();
+                    mPresentationStatsEventLogger.maybeSetAutofillServiceUid(
+                            getAutofillServiceUid());
+                }
                 if (requestNewFillResponseOnViewEnteredIfNecessaryLocked(id, viewState, flags)) {
+                    // If a new request was issued even if previously it was fill dialog request,
+                    // we should end the log event, and start a new one. However, it leaves us
+                    // susceptible to race condition. But since mPresentationStatsEventLogger is
+                    // lock guarded, we should be safe.
+                    if (startedEventWithoutFocus) {
+                        mPresentationStatsEventLogger.logAndEndEvent();
+                        mPresentationStatsEventLogger.startNewEvent();
+                        mPresentationStatsEventLogger.maybeSetAutofillServiceUid(
+                                getAutofillServiceUid());
+                    }
                     return;
                 }
 
@@ -3289,7 +3329,7 @@
                 if (requestShowInlineSuggestionsLocked(response, filterText)) {
                     final ViewState currentView = mViewStates.get(mCurrentViewId);
                     currentView.setState(ViewState.STATE_INLINE_SHOWN);
-                    // TODO(b/137800469): Fix it to log showed only when IME asks for inflation,
+                    // TODO(b/248378401): Fix it to log showed only when IME asks for inflation,
                     // rather than here where framework sends back the response.
                     mService.logDatasetShown(id, mClientState, UI_TYPE_INLINE);
 
@@ -3310,7 +3350,6 @@
 
         synchronized (mLock) {
             mService.logDatasetShown(id, mClientState, UI_TYPE_MENU);
-
             mPresentationStatsEventLogger.maybeSetCountShown(
                     response.getDatasets(), mCurrentViewId);
             mPresentationStatsEventLogger.maybeSetDisplayPresentationType(UI_TYPE_MENU);
diff --git a/services/contentcapture/Android.bp b/services/contentcapture/Android.bp
index 434f239..5392c2c 100644
--- a/services/contentcapture/Android.bp
+++ b/services/contentcapture/Android.bp
@@ -17,6 +17,9 @@
 java_library_static {
     name: "services.contentcapture",
     defaults: ["platform_service_defaults"],
-    srcs: [":services.contentcapture-sources"],
+    srcs: [
+        ":services.contentcapture-sources",
+        "java/**/*.logtags",
+    ],
     libs: ["services.core"],
 }
diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
index 41a7592..c503a5a 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java
@@ -60,6 +60,7 @@
 import android.service.voice.VoiceInteractionManagerInternal;
 import android.util.ArrayMap;
 import android.util.ArraySet;
+import android.util.EventLog;
 import android.util.Slog;
 import android.util.SparseArray;
 import android.util.SparseBooleanArray;
@@ -69,6 +70,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.FrameworkStatsLog;
 import com.android.server.LocalServices;
 import com.android.server.contentcapture.RemoteContentCaptureService.ContentCaptureServiceCallbacks;
@@ -88,6 +90,10 @@
 
     private static final String TAG = ContentCapturePerUserService.class.getSimpleName();
 
+    private static final int EVENT_LOG_CONNECT_STATE_DIED = 0;
+    static final int EVENT_LOG_CONNECT_STATE_CONNECTED = 1;
+    static final int EVENT_LOG_CONNECT_STATE_DISCONNECTED = 2;
+
     @GuardedBy("mLock")
     private final SparseArray<ContentCaptureServerSession> mSessions = new SparseArray<>();
 
@@ -190,9 +196,12 @@
         Slog.w(TAG, "remote service died: " + service);
         synchronized (mLock) {
             mZombie = true;
+            ComponentName serviceComponent = getServiceComponentName();
             writeServiceEvent(
                     FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_REMOTE_SERVICE_DIED,
-                    getServiceComponentName());
+                    serviceComponent);
+            EventLog.writeEvent(EventLogTags.CC_CONNECT_STATE_CHANGED, mUserId,
+                    EVENT_LOG_CONNECT_STATE_DIED, 0);
         }
     }
 
@@ -529,6 +538,15 @@
         return mConditionsByPkg.get(packageName);
     }
 
+    @Nullable
+    ArraySet<String> getContentCaptureAllowlist() {
+        ArraySet<String> allowPackages;
+        synchronized (mLock) {
+            allowPackages = mMaster.mGlobalContentCaptureOptions.getWhitelistedPackages(mUserId);
+        }
+        return allowPackages;
+    }
+
     @GuardedBy("mLock")
     void onActivityEventLocked(@NonNull ComponentName componentName, @ActivityEventType int type) {
         if (mRemoteService == null) {
@@ -617,8 +635,12 @@
 
             ArraySet<String> oldList =
                     mMaster.mGlobalContentCaptureOptions.getWhitelistedPackages(mUserId);
+            EventLog.writeEvent(EventLogTags.CC_CURRENT_ALLOWLIST, mUserId,
+                    CollectionUtils.size(oldList));
 
             mMaster.mGlobalContentCaptureOptions.setWhitelist(mUserId, packages, activities);
+            EventLog.writeEvent(EventLogTags.CC_SET_ALLOWLIST, mUserId,
+                    CollectionUtils.size(packages), CollectionUtils.size(activities));
             writeSetWhitelistEvent(getServiceComponentName(), packages, activities);
 
             updateContentCaptureOptions(oldList);
@@ -699,13 +721,15 @@
         private void updateContentCaptureOptions(@Nullable ArraySet<String> oldList) {
             ArraySet<String> adding = mMaster.mGlobalContentCaptureOptions
                     .getWhitelistedPackages(mUserId);
+            int addingCount = CollectionUtils.size(adding);
+            EventLog.writeEvent(EventLogTags.CC_CURRENT_ALLOWLIST, mUserId, addingCount);
 
             if (oldList != null && adding != null) {
                 adding.removeAll(oldList);
             }
 
-            int N = adding != null ? adding.size() : 0;
-            for (int i = 0; i < N; i++) {
+            EventLog.writeEvent(EventLogTags.CC_UPDATE_OPTIONS, mUserId, addingCount);
+            for (int i = 0; i < addingCount; i++) {
                 String packageName = adding.valueAt(i);
                 ContentCaptureOptions options = mMaster.mGlobalContentCaptureOptions
                         .getOptions(mUserId, packageName);
diff --git a/services/contentcapture/java/com/android/server/contentcapture/EventLogTags.logtags b/services/contentcapture/java/com/android/server/contentcapture/EventLogTags.logtags
new file mode 100644
index 0000000..5218b26
--- /dev/null
+++ b/services/contentcapture/java/com/android/server/contentcapture/EventLogTags.logtags
@@ -0,0 +1,13 @@
+# See system/logging/logcat/event.logtags for a description of the format of this file.
+
+option java_package com.android.server.contentcapture
+
+# ContentCaptureService connection state change, refer to ContentCapturePerUserService
+# for type definition
+53200 cc_connect_state_changed (user|1|5),(type|1|5),(package_count|1|1)
+# Set the package and activity allowlist
+53201 cc_set_allowlist (user|1|5),(package_count|1|1),(activity_count|1|1)
+# Get the current allowlist
+53202 cc_current_allowlist (user|1|5),(count|1|1)
+# update content capture client option with new allow list count
+53203 cc_update_options (user|1|5),(count|1)
diff --git a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
index 1efe55a..3907de4 100644
--- a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
+++ b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java
@@ -31,6 +31,7 @@
 import android.service.contentcapture.IContentCaptureServiceCallback;
 import android.service.contentcapture.IDataShareCallback;
 import android.service.contentcapture.SnapshotData;
+import android.util.EventLog;
 import android.util.Slog;
 import android.view.contentcapture.ContentCaptureContext;
 import android.view.contentcapture.DataRemovalRequest;
@@ -38,6 +39,7 @@
 
 import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService;
 import com.android.internal.os.IResultReceiver;
+import com.android.internal.util.CollectionUtils;
 import com.android.internal.util.FrameworkStatsLog;
 
 final class RemoteContentCaptureService
@@ -88,6 +90,10 @@
                     writeServiceEvent(
                             FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_CONNECTED,
                             mComponentName);
+                    EventLog.writeEvent(EventLogTags.CC_CONNECT_STATE_CHANGED,
+                            mPerUserService.getUserId(),
+                            ContentCapturePerUserService.EVENT_LOG_CONNECT_STATE_CONNECTED,
+                            CollectionUtils.size(mPerUserService.getContentCaptureAllowlist()));
                 } finally {
                     // Update the system-service state, in case the service reconnected after
                     // dying
@@ -98,6 +104,9 @@
                 writeServiceEvent(
                         FrameworkStatsLog.CONTENT_CAPTURE_SERVICE_EVENTS__EVENT__ON_DISCONNECTED,
                         mComponentName);
+                EventLog.writeEvent(EventLogTags.CC_CONNECT_STATE_CHANGED,
+                        mPerUserService.getUserId(),
+                        ContentCapturePerUserService.EVENT_LOG_CONNECT_STATE_DISCONNECTED, 0);
             }
         } catch (Exception e) {
             Slog.w(mTag, "Exception calling onConnectedStateChanged(" + connected + "): " + e);
diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java
index a71f51a..e31c952 100644
--- a/services/core/java/com/android/server/appop/AppOpsService.java
+++ b/services/core/java/com/android/server/appop/AppOpsService.java
@@ -46,6 +46,7 @@
 import static android.app.AppOpsManager.OP_RECEIVE_AMBIENT_TRIGGER_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO;
 import static android.app.AppOpsManager.OP_RECORD_AUDIO_HOTWORD;
+import static android.app.AppOpsManager.OP_VIBRATE;
 import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_FAILED;
 import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_RESUMED;
 import static android.app.AppOpsManager.OnOpStartedListener.START_TYPE_STARTED;
@@ -266,6 +267,7 @@
             OP_PLAY_AUDIO,
             OP_RECORD_AUDIO,
             OP_CAMERA,
+            OP_VIBRATE,
     };
 
     private static final int MAX_UNFORWARDED_OPS = 10;
diff --git a/services/core/java/com/android/server/logcat/LogcatManagerService.java b/services/core/java/com/android/server/logcat/LogcatManagerService.java
index 1bcc21e..43732d4 100644
--- a/services/core/java/com/android/server/logcat/LogcatManagerService.java
+++ b/services/core/java/com/android/server/logcat/LogcatManagerService.java
@@ -16,6 +16,8 @@
 
 package com.android.server.logcat;
 
+import static android.os.Process.getParentPid;
+
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -38,6 +40,8 @@
 import android.util.Slog;
 
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.ILogAccessDialogCallback;
+import com.android.internal.app.LogAccessDialogActivity;
 import com.android.internal.util.ArrayUtils;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -45,6 +49,7 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -98,7 +103,7 @@
     private final Injector mInjector;
     private final Supplier<Long> mClock;
     private final BinderService mBinderService;
-    private final LogcatManagerServiceInternal mLocalService;
+    private final LogAccessDialogCallback mDialogCallback;
     private final Handler mHandler;
     private ActivityManagerInternal mActivityManagerInternal;
     private ILogd mLogdService;
@@ -203,7 +208,8 @@
         }
     }
 
-    final class LogcatManagerServiceInternal {
+    final class LogAccessDialogCallback extends ILogAccessDialogCallback.Stub {
+        @Override
         public void approveAccessForClient(int uid, @NonNull String packageName) {
             final LogAccessClient client = new LogAccessClient(uid, packageName);
             if (DEBUG) {
@@ -213,6 +219,7 @@
             mHandler.sendMessageAtTime(msg, mClock.get());
         }
 
+        @Override
         public void declineAccessForClient(int uid, @NonNull String packageName) {
             final LogAccessClient client = new LogAccessClient(uid, packageName);
             if (DEBUG) {
@@ -299,7 +306,7 @@
         mInjector = injector;
         mClock = injector.createClock();
         mBinderService = new BinderService();
-        mLocalService = new LogcatManagerServiceInternal();
+        mDialogCallback = new LogAccessDialogCallback();
         mHandler = new LogAccessRequestHandler(injector.getLooper(), this);
     }
 
@@ -308,15 +315,14 @@
         try {
             mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
             publishBinderService("logcat", mBinderService);
-            publishLocalService(LogcatManagerServiceInternal.class, mLocalService);
         } catch (Throwable t) {
             Slog.e(TAG, "Could not start the LogcatManagerService.", t);
         }
     }
 
     @VisibleForTesting
-    LogcatManagerServiceInternal getLocalService() {
-        return mLocalService;
+    LogAccessDialogCallback getDialogCallback() {
+        return mDialogCallback;
     }
 
     @VisibleForTesting
@@ -340,13 +346,6 @@
      * access
      */
     private String getPackageName(LogAccessRequest request) {
-        if (mActivityManagerInternal != null) {
-            String packageName = mActivityManagerInternal.getPackageNameByPid(request.mPid);
-            if (packageName != null) {
-                return packageName;
-            }
-        }
-
         PackageManager pm = mContext.getPackageManager();
         if (pm == null) {
             // Decline the logd access if PackageManager is null
@@ -355,15 +354,28 @@
         }
 
         String[] packageNames = pm.getPackagesForUid(request.mUid);
-
         if (ArrayUtils.isEmpty(packageNames)) {
             // Decline the logd access if the app name is unknown
             Slog.e(TAG, "Unknown calling package name, declining the logd access");
             return null;
         }
 
-        String firstPackageName = packageNames[0];
+        if (mActivityManagerInternal != null) {
+            int pid = request.mPid;
+            String packageName = mActivityManagerInternal.getPackageNameByPid(pid);
+            while ((packageName == null || !ArrayUtils.contains(packageNames, packageName))
+                    && pid != -1) {
+                pid = getParentPid(pid);
+                packageName = mActivityManagerInternal.getPackageNameByPid(pid);
+            }
 
+            if (packageName != null && ArrayUtils.contains(packageNames, packageName)) {
+                return packageName;
+            }
+        }
+
+        Arrays.sort(packageNames);
+        String firstPackageName = packageNames[0];
         if (firstPackageName == null || firstPackageName.isEmpty()) {
             // Decline the logd access if the package name from uid is unknown
             Slog.e(TAG, "Unknown calling package name, declining the logd access");
@@ -430,6 +442,7 @@
         mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_PENDING_TIMEOUT, client),
                 mClock.get() + PENDING_CONFIRMATION_TIMEOUT_MILLIS);
         final Intent mIntent = createIntent(client);
+        mIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
         mContext.startActivityAsUser(mIntent, UserHandle.SYSTEM);
     }
 
@@ -530,6 +543,7 @@
 
         intent.putExtra(Intent.EXTRA_PACKAGE_NAME, client.mPackageName);
         intent.putExtra(Intent.EXTRA_UID, client.mUid);
+        intent.putExtra(LogAccessDialogActivity.EXTRA_CALLBACK, mDialogCallback.asBinder());
 
         return intent;
     }
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index e9d5426..b7b3326 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -6546,7 +6546,7 @@
                         if (dependentState == null) {
                             continue;
                         }
-                        if (!Objects.equals(dependentState.getUserStateOrDefault(userId)
+                        if (canSetOverlayPaths(dependentState.getUserStateOrDefault(userId)
                                 .getSharedLibraryOverlayPaths()
                                 .get(libName), newOverlayPaths)) {
                             String dependentPackageName = dependent.getPackageName();
@@ -6562,7 +6562,10 @@
                 }
             }
 
-            outUpdatedPackageNames.add(targetPackageName);
+            if (canSetOverlayPaths(packageState.getUserStateOrDefault(userId).getOverlayPaths(),
+                    newOverlayPaths)) {
+                outUpdatedPackageNames.add(targetPackageName);
+            }
 
             commitPackageStateMutation(null, mutator -> {
                 mutator.forPackage(targetPackageName)
@@ -6593,6 +6596,17 @@
         return true;
     }
 
+    private boolean canSetOverlayPaths(OverlayPaths origPaths, OverlayPaths newPaths) {
+        if (Objects.equals(origPaths, newPaths)) {
+            return false;
+        }
+        if ((origPaths == null && newPaths.isEmpty())
+                || (newPaths == null && origPaths.isEmpty())) {
+            return false;
+        }
+        return true;
+    }
+
     private void maybeUpdateSystemOverlays(String targetPackageName, OverlayPaths newOverlayPaths) {
         if (!mResolverReplaced) {
             if (targetPackageName.equals("android")) {
diff --git a/services/core/java/com/android/server/pm/SuspendPackageHelper.java b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
index 7ad4d22..eebde36 100644
--- a/services/core/java/com/android/server/pm/SuspendPackageHelper.java
+++ b/services/core/java/com/android/server/pm/SuspendPackageHelper.java
@@ -110,12 +110,12 @@
         final SuspendParams newSuspendParams =
                 new SuspendParams(dialogInfo, appExtras, launcherExtras);
 
-        final List<String> changedPackagesList = new ArrayList<>(packageNames.length);
-        final IntArray changedUids = new IntArray(packageNames.length);
-        final IntArray modifiedUids = new IntArray(packageNames.length);
         final List<String> unmodifiablePackages = new ArrayList<>(packageNames.length);
 
-        ArraySet<String> modifiedPackages = new ArraySet<>();
+        final List<String> notifyPackagesList = new ArrayList<>(packageNames.length);
+        final IntArray notifyUids = new IntArray(packageNames.length);
+        final ArraySet<String> changedPackagesList = new ArraySet<>(packageNames.length);
+        final IntArray changedUids = new IntArray(packageNames.length);
 
         final boolean[] canSuspend = suspended
                 ? canSuspendPackageForUser(snapshot, packageNames, userId, callingUid) : null;
@@ -143,21 +143,16 @@
 
             final WatchedArrayMap<String, SuspendParams> suspendParamsMap =
                     packageState.getUserStateOrDefault(userId).getSuspendParams();
-            if (suspended) {
-                if (suspendParamsMap != null && suspendParamsMap.containsKey(packageName)) {
-                    final SuspendParams suspendParams = suspendParamsMap.get(packageName);
-                    // Skip if there's no changes
-                    if (suspendParams != null
-                            && Objects.equals(suspendParams.getDialogInfo(), dialogInfo)
-                            && Objects.equals(suspendParams.getAppExtras(), appExtras)
-                            && Objects.equals(suspendParams.getLauncherExtras(),
-                            launcherExtras)) {
-                        // Carried over API behavior, must notify change even if no change
-                        changedPackagesList.add(packageName);
-                        changedUids.add(UserHandle.getUid(userId, packageState.getAppId()));
-                        continue;
-                    }
-                }
+
+            SuspendParams oldSuspendParams = suspendParamsMap == null
+                    ? null : suspendParamsMap.get(packageName);
+            boolean changed = !Objects.equals(oldSuspendParams, newSuspendParams);
+
+            if (suspended && !changed) {
+                // Carried over API behavior, must notify change even if no change
+                notifyPackagesList.add(packageName);
+                notifyUids.add(UserHandle.getUid(userId, packageState.getAppId()));
+                continue;
             }
 
             // If only the callingPackage is suspending this package,
@@ -166,18 +161,21 @@
                     && CollectionUtils.size(suspendParamsMap) == 1
                     && suspendParamsMap.containsKey(callingPackage);
             if (suspended || packageUnsuspended) {
+                // Always notify of a suspend call + notify when fully unsuspended
+                notifyPackagesList.add(packageName);
+                notifyUids.add(UserHandle.getUid(userId, packageState.getAppId()));
+            }
+
+            if (changed) {
                 changedPackagesList.add(packageName);
                 changedUids.add(UserHandle.getUid(userId, packageState.getAppId()));
             }
-
-            modifiedPackages.add(packageName);
-            modifiedUids.add(UserHandle.getUid(userId, packageState.getAppId()));
         }
 
         mPm.commitPackageStateMutation(null, mutator -> {
-            final int size = modifiedPackages.size();
+            final int size = changedPackagesList.size();
             for (int index = 0; index < size; index++) {
-                final String packageName  = modifiedPackages.valueAt(index);
+                final String packageName  = changedPackagesList.valueAt(index);
                 final PackageUserStateWrite userState = mutator.forPackage(packageName)
                         .userState(userId);
                 if (suspended) {
@@ -190,19 +188,19 @@
 
         final Computer newSnapshot = mPm.snapshotComputer();
 
-        if (!changedPackagesList.isEmpty()) {
-            final String[] changedPackages = changedPackagesList.toArray(new String[0]);
+        if (!notifyPackagesList.isEmpty()) {
+            final String[] notifyPackages = notifyPackagesList.toArray(new String[0]);
             sendPackagesSuspendedForUser(newSnapshot,
                     suspended ? Intent.ACTION_PACKAGES_SUSPENDED
                             : Intent.ACTION_PACKAGES_UNSUSPENDED,
-                    changedPackages, changedUids.toArray(), userId);
-            sendMyPackageSuspendedOrUnsuspended(changedPackages, suspended, userId);
+                    notifyPackages, notifyUids.toArray(), userId);
+            sendMyPackageSuspendedOrUnsuspended(notifyPackages, suspended, userId);
             mPm.scheduleWritePackageRestrictions(userId);
         }
         // Send the suspension changed broadcast to ensure suspension state is not stale.
-        if (!modifiedPackages.isEmpty()) {
+        if (!changedPackagesList.isEmpty()) {
             sendPackagesSuspendedForUser(newSnapshot, Intent.ACTION_PACKAGES_SUSPENSION_CHANGED,
-                    modifiedPackages.toArray(new String[0]), modifiedUids.toArray(), userId);
+                    changedPackagesList.toArray(new String[0]), changedUids.toArray(), userId);
         }
         return unmodifiablePackages.toArray(new String[0]);
     }
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java
index dc4bdaa..ce1157e 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareLogging.java
@@ -18,6 +18,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.content.Context;
 import android.media.permission.Identity;
 import android.media.permission.IdentityContext;
 import android.media.soundtrigger.ModelParameterRange;
@@ -33,6 +34,8 @@
 import android.os.RemoteException;
 import android.util.Log;
 
+import com.android.internal.util.LatencyTracker;
+
 import java.io.PrintWriter;
 import java.text.SimpleDateFormat;
 import java.util.Date;
@@ -65,9 +68,12 @@
 public class SoundTriggerMiddlewareLogging implements ISoundTriggerMiddlewareInternal, Dumpable {
     private static final String TAG = "SoundTriggerMiddlewareLogging";
     private final @NonNull ISoundTriggerMiddlewareInternal mDelegate;
+    private final @NonNull Context mContext;
 
-    public SoundTriggerMiddlewareLogging(@NonNull ISoundTriggerMiddlewareInternal delegate) {
+    public SoundTriggerMiddlewareLogging(@NonNull Context context,
+            @NonNull ISoundTriggerMiddlewareInternal delegate) {
         mDelegate = delegate;
+        mContext = context;
     }
 
     @Override
@@ -298,6 +304,7 @@
                     int captureSession)
                     throws RemoteException {
                 try {
+                    startKeyphraseEventLatencyTracking(event);
                     mCallbackDelegate.onPhraseRecognition(modelHandle, event, captureSession);
                     logVoidReturn("onPhraseRecognition", modelHandle, event);
                 } catch (Exception e) {
@@ -347,6 +354,26 @@
                 logVoidReturnWithObject(this, mOriginatorIdentity, methodName, args);
             }
 
+            /**
+             * Starts the latency tracking log for keyphrase hotword invocation.
+             * The measurement covers from when the SoundTrigger HAL emits an event to when the
+             * {@link android.service.voice.VoiceInteractionSession} system UI view is shown.
+             */
+            private void startKeyphraseEventLatencyTracking(PhraseRecognitionEvent event) {
+                String latencyTrackerTag = null;
+                if (event.phraseExtras.length > 0) {
+                    latencyTrackerTag = "KeyphraseId=" + event.phraseExtras[0].id;
+                }
+                LatencyTracker latencyTracker = LatencyTracker.getInstance(mContext);
+                // To avoid adding cancel to all of the different failure modes between here and
+                // showing the system UI, we defensively cancel once.
+                // Either we hit the LatencyTracker timeout of 15 seconds or we defensively cancel
+                // here if any error occurs.
+                latencyTracker.onActionCancel(LatencyTracker.ACTION_SHOW_VOICE_INTERACTION);
+                latencyTracker.onActionStart(LatencyTracker.ACTION_SHOW_VOICE_INTERACTION,
+                        latencyTrackerTag);
+            }
+
             @Override
             public IBinder asBinder() {
                 return mCallbackDelegate.asBinder();
diff --git a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java
index 1995e54..807ed14 100644
--- a/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java
+++ b/services/core/java/com/android/server/soundtrigger_middleware/SoundTriggerMiddlewareService.java
@@ -20,14 +20,14 @@
 
 import android.annotation.NonNull;
 import android.content.Context;
-import android.media.soundtrigger.ModelParameterRange;
-import android.media.soundtrigger.PhraseSoundModel;
-import android.media.soundtrigger.RecognitionConfig;
-import android.media.soundtrigger.SoundModel;
 import android.media.permission.ClearCallingIdentityContext;
 import android.media.permission.Identity;
 import android.media.permission.PermissionUtil;
 import android.media.permission.SafeCloseable;
+import android.media.soundtrigger.ModelParameterRange;
+import android.media.soundtrigger.PhraseSoundModel;
+import android.media.soundtrigger.RecognitionConfig;
+import android.media.soundtrigger.SoundModel;
 import android.media.soundtrigger_middleware.ISoundTriggerCallback;
 import android.media.soundtrigger_middleware.ISoundTriggerMiddlewareService;
 import android.media.soundtrigger_middleware.ISoundTriggerModule;
@@ -226,12 +226,13 @@
             HalFactory[] factories = new HalFactory[]{new DefaultHalFactory()};
 
             publishBinderService(Context.SOUND_TRIGGER_MIDDLEWARE_SERVICE,
-                    new SoundTriggerMiddlewareService(new SoundTriggerMiddlewareLogging(
-                            new SoundTriggerMiddlewarePermission(
-                                    new SoundTriggerMiddlewareValidation(
-                                            new SoundTriggerMiddlewareImpl(factories,
-                                                    new AudioSessionProviderImpl())),
-                                    getContext())), getContext()));
+                    new SoundTriggerMiddlewareService(
+                            new SoundTriggerMiddlewareLogging(getContext(),
+                                new SoundTriggerMiddlewarePermission(
+                                        new SoundTriggerMiddlewareValidation(
+                                                new SoundTriggerMiddlewareImpl(factories,
+                                                        new AudioSessionProviderImpl())),
+                                        getContext())), getContext()));
         }
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index e9fbeb6..b8486e7 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2764,6 +2764,7 @@
 
         final StartingSurfaceController.StartingSurface surface;
         final StartingData startingData = mStartingData;
+        final WindowState startingWindow = mStartingWindow;
         if (mStartingData != null) {
             surface = mStartingSurface;
             mStartingData = null;
@@ -2782,21 +2783,31 @@
             return;
         }
 
-
         ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Schedule remove starting %s startingWindow=%s"
                 + " startingView=%s Callers=%s", this, mStartingWindow, mStartingSurface,
                 Debug.getCallers(5));
-
+        final boolean removeWithAnimate = prepareAnimation && startingData.needRevealAnimation();
         final Runnable removeSurface = () -> {
             ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "Removing startingView=%s", surface);
             try {
-                surface.remove(prepareAnimation && startingData.needRevealAnimation());
+                surface.remove(removeWithAnimate);
             } catch (Exception e) {
                 Slog.w(TAG_WM, "Exception when removing starting window", e);
             }
         };
-
-        removeSurface.run();
+        if (removeWithAnimate && mTransitionController.inCollectingTransition(startingWindow)
+                && startingWindow.cancelAndRedraw()) {
+            // Defer remove starting window after transition start.
+            // If splash screen window was in collecting, the client side is unable to draw because
+            // of Session#cancelDraw, which will blocking the remove animation.
+            startingWindow.mSyncTransaction.addTransactionCommittedListener(Runnable::run, () -> {
+                synchronized (mAtmService.mGlobalLock) {
+                    removeSurface.run();
+                }
+            });
+        } else {
+            removeSurface.run();
+        }
     }
 
     /**
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 9456f0f..2e1477d 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -25,6 +25,7 @@
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_DELETE_TASK_FRAGMENT;
+import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_FINISH_ACTIVITY;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_LAUNCH_TASK;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_PENDING_INTENT;
 import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_INSETS_PROVIDER;
@@ -1007,6 +1008,20 @@
                         isInLockTaskMode);
                 break;
             }
+            case HIERARCHY_OP_TYPE_FINISH_ACTIVITY: {
+                final ActivityRecord activity = ActivityRecord.forTokenLocked(hop.getContainer());
+                if (activity == null || activity.finishing) {
+                    break;
+                }
+                if (activity.isVisible()) {
+                    // Prevent the transition from being executed too early if the activity is
+                    // visible.
+                    activity.finishIfPossible("finish-activity-op", false /* oomAdj */);
+                } else {
+                    activity.destroyIfPossible("finish-activity-op");
+                }
+                break;
+            }
             case HIERARCHY_OP_TYPE_LAUNCH_TASK: {
                 mService.mAmInternal.enforceCallingPermission(START_TASKS_FROM_RECENTS,
                         "launchTask HierarchyOp");
@@ -1620,6 +1635,9 @@
                                 organizer);
                     }
                     break;
+                case HIERARCHY_OP_TYPE_FINISH_ACTIVITY:
+                    // Allow finish activity if it has the activity token.
+                    break;
                 default:
                     // Other types of hierarchy changes are not allowed.
                     String msg = "Permission Denial: " + func + " from pid="
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index 87bf4b8..2ee4af5 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -338,8 +338,6 @@
             "com.android.server.contentcapture.ContentCaptureManagerService";
     private static final String TRANSLATION_MANAGER_SERVICE_CLASS =
             "com.android.server.translation.TranslationManagerService";
-    private static final String SELECTION_TOOLBAR_MANAGER_SERVICE_CLASS =
-            "com.android.server.selectiontoolbar.SelectionToolbarManagerService";
     private static final String MUSIC_RECOGNITION_MANAGER_SERVICE_CLASS =
             "com.android.server.musicrecognition.MusicRecognitionManagerService";
     private static final String SYSTEM_CAPTIONS_MANAGER_SERVICE_CLASS =
@@ -2628,11 +2626,6 @@
             Slog.d(TAG, "TranslationService not defined by OEM");
         }
 
-        // Selection toolbar service
-        t.traceBegin("StartSelectionToolbarManagerService");
-        mSystemServiceManager.startService(SELECTION_TOOLBAR_MANAGER_SERVICE_CLASS);
-        t.traceEnd();
-
         // NOTE: ClipboardService depends on ContentCapture and Autofill
         t.traceBegin("StartClipboardService");
         mSystemServiceManager.startService(ClipboardService.class);
diff --git a/services/selectiontoolbar/Android.bp b/services/selectiontoolbar/Android.bp
deleted file mode 100644
index cc6405f..0000000
--- a/services/selectiontoolbar/Android.bp
+++ /dev/null
@@ -1,22 +0,0 @@
-package {
-    // See: http://go/android-license-faq
-    // A large-scale-change added 'default_applicable_licenses' to import
-    // all of the 'license_kinds' from "frameworks_base_license"
-    // to get the below license kinds:
-    //   SPDX-license-identifier-Apache-2.0
-    default_applicable_licenses: ["frameworks_base_license"],
-}
-
-filegroup {
-    name: "services.selectiontoolbar-sources",
-    srcs: ["java/**/*.java"],
-    path: "java",
-    visibility: ["//frameworks/base/services"],
-}
-
-java_library_static {
-    name: "services.selectiontoolbar",
-    defaults: ["platform_service_defaults"],
-    srcs: [":services.selectiontoolbar-sources"],
-    libs: ["services.core"],
-}
diff --git a/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
index f330017..2cd5314 100644
--- a/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/logcat/LogcatManagerServiceTest.java
@@ -16,6 +16,8 @@
 
 package com.android.server.logcat;
 
+import static android.os.Process.INVALID_UID;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
@@ -28,6 +30,7 @@
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
 import android.content.ContextWrapper;
+import android.content.pm.PackageManager;
 import android.os.ILogd;
 import android.os.Looper;
 import android.os.UserHandle;
@@ -69,10 +72,12 @@
     @Mock
     private ActivityManagerInternal mActivityManagerInternalMock;
     @Mock
+    private PackageManager mPackageManagerMock;
+    @Mock
     private ILogd mLogdMock;
 
     private LogcatManagerService mService;
-    private LogcatManagerService.LogcatManagerServiceInternal mLocalService;
+    private LogcatManagerService.LogAccessDialogCallback mDialogCallback;
     private ContextWrapper mContextSpy;
     private OffsettableClock mClock;
     private TestLooper mTestLooper;
@@ -81,10 +86,17 @@
     public void setUp() throws Exception {
         MockitoAnnotations.initMocks(this);
         addLocalServiceMock(ActivityManagerInternal.class, mActivityManagerInternalMock);
+        when(mActivityManagerInternalMock.getInstrumentationSourceUid(anyInt()))
+                .thenReturn(INVALID_UID);
+
         mContextSpy = spy(new ContextWrapper(ApplicationProvider.getApplicationContext()));
         mClock = new OffsettableClock.Stopped();
         mTestLooper = new TestLooper(mClock::now);
-
+        when(mContextSpy.getPackageManager()).thenReturn(mPackageManagerMock);
+        when(mPackageManagerMock.getPackagesForUid(APP1_UID)).thenReturn(
+                new String[]{APP1_PACKAGE_NAME});
+        when(mPackageManagerMock.getPackagesForUid(APP2_UID)).thenReturn(
+                new String[]{APP2_PACKAGE_NAME});
         when(mActivityManagerInternalMock.getPackageNameByPid(APP1_PID)).thenReturn(
                 APP1_PACKAGE_NAME);
         when(mActivityManagerInternalMock.getPackageNameByPid(APP2_PID)).thenReturn(
@@ -106,7 +118,7 @@
                 return mLogdMock;
             }
         });
-        mLocalService = mService.getLocalService();
+        mDialogCallback = mService.getDialogCallback();
         mService.onStart();
     }
 
@@ -136,6 +148,20 @@
     }
 
     @Test
+    public void test_RequestFromBackground_ApprovedIfInstrumented() throws Exception {
+        when(mActivityManagerInternalMock.getInstrumentationSourceUid(APP1_UID))
+                .thenReturn(APP1_UID);
+        when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
+                ActivityManager.PROCESS_STATE_RECEIVER);
+        mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
+        mTestLooper.dispatchAll();
+
+        verify(mLogdMock).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mLogdMock, never()).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
+        verify(mContextSpy, never()).startActivityAsUser(any(), any());
+    }
+
+    @Test
     public void test_RequestFromForegroundService_DeclinedWithoutPrompt() throws Exception {
         when(mActivityManagerInternalMock.getUidProcessState(APP1_UID)).thenReturn(
                 ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
@@ -181,7 +207,7 @@
         mTestLooper.dispatchAll();
         verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
 
-        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
@@ -196,7 +222,7 @@
         mTestLooper.dispatchAll();
         verify(mContextSpy, times(1)).startActivityAsUser(any(), eq(UserHandle.SYSTEM));
 
-        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         verify(mLogdMock, never()).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
@@ -214,7 +240,7 @@
         verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
         verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
 
-        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         verify(mLogdMock, times(1)).approve(APP1_UID, APP1_GID, APP1_PID, FD1);
@@ -234,7 +260,7 @@
         verify(mLogdMock, never()).approve(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
         verify(mLogdMock, never()).decline(eq(APP1_UID), eq(APP1_GID), eq(APP1_PID), anyInt());
 
-        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         verify(mLogdMock, times(1)).decline(APP1_UID, APP1_GID, APP1_PID, FD1);
@@ -249,7 +275,7 @@
                 ActivityManager.PROCESS_STATE_TOP);
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
         mTestLooper.dispatchAll();
-        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
@@ -267,7 +293,7 @@
                 ActivityManager.PROCESS_STATE_TOP);
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
         mTestLooper.dispatchAll();
-        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD2);
@@ -287,7 +313,7 @@
                 ActivityManager.PROCESS_STATE_TOP);
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
         mTestLooper.dispatchAll();
-        mLocalService.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.approveAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         mService.getBinderService().startThread(APP2_UID, APP2_GID, APP2_PID, FD2);
@@ -304,7 +330,7 @@
                 ActivityManager.PROCESS_STATE_TOP);
         mService.getBinderService().startThread(APP1_UID, APP1_GID, APP1_PID, FD1);
         mTestLooper.dispatchAll();
-        mLocalService.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
+        mDialogCallback.declineAccessForClient(APP1_UID, APP1_PACKAGE_NAME);
         mTestLooper.dispatchAll();
 
         advanceTime(LogcatManagerService.STATUS_EXPIRATION_TIMEOUT_MILLIS);
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 61cf8cc..1404de2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java
@@ -730,6 +730,16 @@
     }
 
     @Test
+    public void testApplyTransaction_finishActivity() {
+        final ActivityRecord activity = createActivityRecord(mDisplayContent);
+
+        mTransaction.finishActivity(activity.token);
+        assertApplyTransactionAllowed(mTransaction);
+
+        assertTrue(activity.finishing);
+    }
+
+    @Test
     public void testApplyTransaction_skipTransactionForUnregisterOrganizer() {
         mController.unregisterOrganizer(mIOrganizer);
         final ActivityRecord ownerActivity = createActivityRecord(mDisplayContent);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
index bde9c3d..a6e1a32 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/HotwordDetectionConnection.java
@@ -22,6 +22,7 @@
 import static android.service.voice.HotwordDetectedResult.EXTRA_PROXIMITY_METERS;
 import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_EXTERNAL;
 import static android.service.voice.HotwordDetectionService.AUDIO_SOURCE_MICROPHONE;
+import static android.service.voice.HotwordDetectionService.ENABLE_PROXIMITY_RESULT;
 import static android.service.voice.HotwordDetectionService.INITIALIZATION_STATUS_SUCCESS;
 import static android.service.voice.HotwordDetectionService.INITIALIZATION_STATUS_UNKNOWN;
 import static android.service.voice.HotwordDetectionService.KEY_INITIALIZATION_STATUS;
@@ -185,7 +186,7 @@
     final int mUser;
     final Context mContext;
 
-    @Nullable final AttentionManagerInternal mAttentionManagerInternal;
+    @Nullable AttentionManagerInternal mAttentionManagerInternal = null;
 
     final AttentionManagerInternal.ProximityUpdateCallbackInternal mProximityCallbackInternal =
             this::setProximityMeters;
@@ -240,9 +241,11 @@
         mServiceConnectionFactory = new ServiceConnectionFactory(intent, bindInstantServiceAllowed);
 
         mRemoteHotwordDetectionService = mServiceConnectionFactory.createLocked();
-        mAttentionManagerInternal = LocalServices.getService(AttentionManagerInternal.class);
-        if (mAttentionManagerInternal != null) {
-            mAttentionManagerInternal.onStartProximityUpdates(mProximityCallbackInternal);
+        if (ENABLE_PROXIMITY_RESULT) {
+            mAttentionManagerInternal = LocalServices.getService(AttentionManagerInternal.class);
+            if (mAttentionManagerInternal != null) {
+                mAttentionManagerInternal.onStartProximityUpdates(mProximityCallbackInternal);
+            }
         }
 
         mLastRestartInstant = Instant.now();
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 0ce0265..3da4711 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -95,6 +95,7 @@
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.util.DumpUtils;
+import com.android.internal.util.LatencyTracker;
 import com.android.server.FgThread;
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
@@ -191,6 +192,8 @@
             mSoundTriggerInternal = LocalServices.getService(SoundTriggerInternal.class);
         } else if (phase == PHASE_THIRD_PARTY_APPS_CAN_START) {
             mServiceStub.systemRunning(isSafeMode());
+        } else if (phase == PHASE_BOOT_COMPLETED) {
+            mServiceStub.registerVoiceInteractionSessionListener(mLatencyLoggingListener);
         }
     }
 
@@ -2334,4 +2337,36 @@
             }
         };
     }
+
+    /**
+     * End the latency tracking log for keyphrase hotword invocation.
+     * The measurement covers from when the SoundTrigger HAL emits an event, captured in
+     * {@link com.android.server.soundtrigger_middleware.SoundTriggerMiddlewareLogging}
+     * to when the {@link android.service.voice.VoiceInteractionSession} system UI view is shown.
+     */
+    private final IVoiceInteractionSessionListener mLatencyLoggingListener =
+            new IVoiceInteractionSessionListener.Stub() {
+                @Override
+                public void onVoiceSessionShown() throws RemoteException {}
+
+                @Override
+                public void onVoiceSessionHidden() throws RemoteException {}
+
+                @Override
+                public void onVoiceSessionWindowVisibilityChanged(boolean visible)
+                        throws RemoteException {
+                    if (visible) {
+                        LatencyTracker.getInstance(mContext)
+                                .onActionEnd(LatencyTracker.ACTION_SHOW_VOICE_INTERACTION);
+                    }
+                }
+
+                @Override
+                public void onSetUiHints(Bundle args) throws RemoteException {}
+
+                @Override
+                public IBinder asBinder() {
+                    return mServiceStub;
+                }
+            };
 }