Merge "Enable NotificationMemoryMonitor dumps in bug reports" into tm-qpr-dev
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 855366a..81c3e89 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -87,10 +87,12 @@
 import android.os.PersistableBundle;
 import android.os.Process;
 import android.os.RemoteException;
+import android.os.ServiceManager;
 import android.os.ServiceManager.ServiceNotFoundException;
 import android.os.StrictMode;
 import android.os.Trace;
 import android.os.UserHandle;
+import android.service.voice.VoiceInteractionSession;
 import android.text.Selection;
 import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
@@ -154,6 +156,7 @@
 import com.android.internal.R;
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.IVoiceInteractionManagerService;
 import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.app.ToolbarActionBar;
 import com.android.internal.app.WindowDecorActionBar;
@@ -1601,6 +1604,25 @@
         return callbacks;
     }
 
+    private void notifyVoiceInteractionManagerServiceActivityEvent(
+            @VoiceInteractionSession.VoiceInteractionActivityEventType int type) {
+
+        final IVoiceInteractionManagerService service =
+                IVoiceInteractionManagerService.Stub.asInterface(
+                        ServiceManager.getService(Context.VOICE_INTERACTION_MANAGER_SERVICE));
+        if (service == null) {
+            Log.w(TAG, "notifyVoiceInteractionManagerServiceActivityEvent: Can not get "
+                    + "VoiceInteractionManagerService");
+            return;
+        }
+
+        try {
+            service.notifyActivityEventChanged(mToken, type);
+        } catch (RemoteException e) {
+            // Empty
+        }
+    }
+
     /**
      * Called when the activity is starting.  This is where most initialization
      * should go: calling {@link #setContentView(int)} to inflate the
@@ -1876,6 +1898,9 @@
         mCalled = true;
 
         notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_START);
+
+        notifyVoiceInteractionManagerServiceActivityEvent(
+                VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_START);
     }
 
     /**
@@ -2019,6 +2044,12 @@
         final Window win = getWindow();
         if (win != null) win.makeActive();
         if (mActionBar != null) mActionBar.setShowHideAnimationEnabled(true);
+
+        // Because the test case "com.android.launcher3.jank.BinderTests#testPressHome" doesn't
+        // allow any binder call in onResume, we call this method in onPostResume.
+        notifyVoiceInteractionManagerServiceActivityEvent(
+                VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_RESUME);
+
         mCalled = true;
     }
 
@@ -2394,6 +2425,10 @@
         getAutofillClientController().onActivityPaused();
 
         notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_PAUSE);
+
+        notifyVoiceInteractionManagerServiceActivityEvent(
+                VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE);
+
         mCalled = true;
     }
 
@@ -2623,6 +2658,9 @@
 
         getAutofillClientController().onActivityStopped(mIntent, mChangingConfigurations);
         mEnterAnimationComplete = false;
+
+        notifyVoiceInteractionManagerServiceActivityEvent(
+                VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_STOP);
     }
 
     /**
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index f2ea060..c95a7de 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -731,10 +731,9 @@
      */
     public interface VoiceInteractionManagerProvider {
         /**
-         * Notifies the service when a high-level activity event has been changed, for example,
-         * an activity was resumed or stopped.
+         * Notifies the service when an activity is destroyed.
          */
-        void notifyActivityEventChanged();
+        void notifyActivityDestroyed(IBinder activityToken);
     }
 
     /**
diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java
index a432b8d..fd94969 100644
--- a/core/java/android/appwidget/AppWidgetManager.java
+++ b/core/java/android/appwidget/AppWidgetManager.java
@@ -42,12 +42,15 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.util.DisplayMetrics;
+import android.util.Log;
 import android.widget.RemoteViews;
 
 import com.android.internal.appwidget.IAppWidgetService;
+import com.android.internal.os.BackgroundThread;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * Updates AppWidget state; gets information about installed AppWidget providers and other
@@ -63,6 +66,7 @@
 @RequiresFeature(PackageManager.FEATURE_APP_WIDGETS)
 public class AppWidgetManager {
 
+
     /**
      * Activity action to launch from your {@link AppWidgetHost} activity when you want to
      * pick an AppWidget to display.  The AppWidget picker activity will be launched.
@@ -332,6 +336,17 @@
     public static final String ACTION_APPWIDGET_UPDATE = "android.appwidget.action.APPWIDGET_UPDATE";
 
     /**
+     * A combination broadcast of APPWIDGET_ENABLED and APPWIDGET_UPDATE.
+     * Sent during boot time and when the host is binding the widget for the very first time
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @BroadcastBehavior(explicitOnly = true)
+    public static final String ACTION_APPWIDGET_ENABLE_AND_UPDATE = "android.appwidget.action"
+            + ".APPWIDGET_ENABLE_AND_UPDATE";
+
+    /**
      * Sent when the custom extras for an AppWidget change.
      *
      * <p class="note">This is a protected intent that can only be sent
@@ -456,6 +471,8 @@
     public static final String ACTION_APPWIDGET_HOST_RESTORED
             = "android.appwidget.action.APPWIDGET_HOST_RESTORED";
 
+    private static final String TAG = "AppWidgetManager";
+
     /**
      * An intent extra that contains multiple appWidgetIds.  These are id values as
      * they were provided to the application during a recent restore from backup.  It is
@@ -511,6 +528,26 @@
         mPackageName = context.getOpPackageName();
         mService = service;
         mDisplayMetrics = context.getResources().getDisplayMetrics();
+        if (mService == null) {
+            return;
+        }
+        BackgroundThread.getExecutor().execute(() -> {
+            try {
+                mService.notifyProviderInheritance(getInstalledProvidersForPackage(mPackageName,
+                        null)
+                        .stream().filter(Objects::nonNull)
+                        .map(info -> info.provider).filter(p -> {
+                            try {
+                                Class clazz = Class.forName(p.getClassName());
+                                return AppWidgetProvider.class.isAssignableFrom(clazz);
+                            } catch (Exception e) {
+                                return false;
+                            }
+                        }).toArray(ComponentName[]::new));
+            } catch (Exception e) {
+                Log.e(TAG, "Nofity service of inheritance info", e);
+            }
+        });
     }
 
     /**
diff --git a/core/java/android/appwidget/AppWidgetProvider.java b/core/java/android/appwidget/AppWidgetProvider.java
index a5d2198..3344ebc 100644
--- a/core/java/android/appwidget/AppWidgetProvider.java
+++ b/core/java/android/appwidget/AppWidgetProvider.java
@@ -58,7 +58,12 @@
         // Protect against rogue update broadcasts (not really a security issue,
         // just filter bad broacasts out so subclasses are less likely to crash).
         String action = intent.getAction();
-        if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
+        if (AppWidgetManager.ACTION_APPWIDGET_ENABLE_AND_UPDATE.equals(action)) {
+            this.onReceive(context, new Intent(intent)
+                    .setAction(AppWidgetManager.ACTION_APPWIDGET_ENABLED));
+            this.onReceive(context, new Intent(intent)
+                    .setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE));
+        } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)) {
             Bundle extras = intent.getExtras();
             if (extras != null) {
                 int[] appWidgetIds = extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS);
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 0fd164d..085bfca 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -918,7 +918,6 @@
         }
     }
 
-
     /**
      * Forwards BiometricStateListener to FingerprintService
      * @param listener new BiometricStateListener being added
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index c9fd129..a955dbb 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9793,6 +9793,13 @@
                 "fingerprint_side_fps_auth_downtime";
 
         /**
+         * Whether or not a SFPS device is required to be interactive for auth to unlock the device.
+         * @hide
+         */
+        public static final String SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED =
+                "sfps_require_screen_on_to_auth_enabled";
+
+        /**
          * Whether or not debugging is enabled.
          * @hide
          */
diff --git a/core/java/android/service/voice/VoiceInteractionSession.java b/core/java/android/service/voice/VoiceInteractionSession.java
index df727e9..48f732e 100644
--- a/core/java/android/service/voice/VoiceInteractionSession.java
+++ b/core/java/android/service/voice/VoiceInteractionSession.java
@@ -19,6 +19,7 @@
 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
 
 import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
 import android.annotation.IntRange;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -71,6 +72,8 @@
 
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -143,6 +146,25 @@
      */
     public static final int SHOW_SOURCE_AUTOMOTIVE_SYSTEM_UI = 1 << 7;
 
+    /** @hide */
+    public static final int VOICE_INTERACTION_ACTIVITY_EVENT_START = 1;
+    /** @hide */
+    public static final int VOICE_INTERACTION_ACTIVITY_EVENT_RESUME = 2;
+    /** @hide */
+    public static final int VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE = 3;
+    /** @hide */
+    public static final int VOICE_INTERACTION_ACTIVITY_EVENT_STOP = 4;
+
+    /** @hide */
+    @IntDef(prefix = { "VOICE_INTERACTION_ACTIVITY_EVENT_" }, value = {
+            VOICE_INTERACTION_ACTIVITY_EVENT_START,
+            VOICE_INTERACTION_ACTIVITY_EVENT_RESUME,
+            VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE,
+            VOICE_INTERACTION_ACTIVITY_EVENT_STOP
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface VoiceInteractionActivityEventType{}
+
     final Context mContext;
     final HandlerCaller mHandlerCaller;
 
diff --git a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
index 681693b..bd51f12 100644
--- a/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
+++ b/core/java/com/android/internal/app/IVoiceInteractionManagerService.aidl
@@ -18,6 +18,8 @@
 
 import android.content.ComponentName;
 import android.content.Intent;
+import android.hardware.soundtrigger.KeyphraseMetadata;
+import android.hardware.soundtrigger.SoundTrigger;
 import android.media.AudioFormat;
 import android.media.permission.Identity;
 import android.os.Bundle;
@@ -25,18 +27,17 @@
 import android.os.PersistableBundle;
 import android.os.RemoteCallback;
 import android.os.SharedMemory;
+import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
+import android.service.voice.IVoiceInteractionService;
+import android.service.voice.IVoiceInteractionSession;
+import android.service.voice.VisibleActivityInfo;
 
 import com.android.internal.app.IHotwordRecognitionStatusCallback;
 import com.android.internal.app.IVoiceActionCheckCallback;
-import com.android.internal.app.IVoiceInteractionSessionShowCallback;
-import com.android.internal.app.IVoiceInteractor;
 import com.android.internal.app.IVoiceInteractionSessionListener;
+import com.android.internal.app.IVoiceInteractionSessionShowCallback;
 import com.android.internal.app.IVoiceInteractionSoundTriggerSession;
-import android.hardware.soundtrigger.KeyphraseMetadata;
-import android.hardware.soundtrigger.SoundTrigger;
-import android.service.voice.IVoiceInteractionService;
-import android.service.voice.IVoiceInteractionSession;
-import android.service.voice.IMicrophoneHotwordDetectionVoiceInteractionCallback;
+import com.android.internal.app.IVoiceInteractor;
 
 interface IVoiceInteractionManagerService {
     void showSession(in Bundle sessionArgs, int flags);
@@ -289,4 +290,14 @@
      * Notifies when the session window is shown or hidden.
      */
     void setSessionWindowVisible(in IBinder token, boolean visible);
+
+    /**
+     * Notifies when the Activity lifecycle event changed.
+     *
+     * @param activityToken The token of activity.
+     * @param type The type of lifecycle event of the activity lifecycle.
+     */
+    oneway void notifyActivityEventChanged(
+            in IBinder activityToken,
+            int type);
 }
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index 6c689ff..44997b4 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -578,6 +578,11 @@
      */
     public static final String CLIPBOARD_OVERLAY_SHOW_ACTIONS = "clipboard_overlay_show_actions";
 
+    /**
+     * (boolean) Whether to combine the broadcasts APPWIDGET_ENABLED and APPWIDGET_UPDATE
+     */
+    public static final String COMBINED_BROADCAST_ENABLED = "combined_broadcast_enabled";
+
     private SystemUiDeviceConfigFlags() {
     }
 }
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index f7467b5..7787b7c 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -142,6 +142,7 @@
     <protected-broadcast android:name="android.appwidget.action.APPWIDGET_ENABLED" />
     <protected-broadcast android:name="android.appwidget.action.APPWIDGET_HOST_RESTORED" />
     <protected-broadcast android:name="android.appwidget.action.APPWIDGET_RESTORED" />
+    <protected-broadcast android:name="android.appwidget.action.APPWIDGET_ENABLE_AND_UPDATE" />
 
     <protected-broadcast android:name="android.os.action.SETTING_RESTORED" />
 
diff --git a/core/res/res/values/bools.xml b/core/res/res/values/bools.xml
index 988303e..4b27bf2 100644
--- a/core/res/res/values/bools.xml
+++ b/core/res/res/values/bools.xml
@@ -31,5 +31,4 @@
          lockscreen, setting this to true should come with customized drawables. -->
     <bool name="use_lock_pattern_drawable">false</bool>
     <bool name="resolver_landscape_phone">true</bool>
-    <bool name="system_server_plays_face_haptics">true</bool>
 </resources>
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index aa9a949..bd8a429 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -2491,8 +2491,10 @@
          assistant activities (ACTIVITY_TYPE_ASSISTANT) -->
     <bool name="config_dismissDreamOnActivityStart">false</bool>
 
-    <!-- The prefix of dream component names that are loggable. If empty, logs "other" for all. -->
-    <string name="config_loggable_dream_prefix" translatable="false"></string>
+    <!-- The prefixes of dream component names that are loggable.
+         Matched against ComponentName#flattenToString() for dream components.
+         If empty, logs "other" for all. -->
+    <string-array name="config_loggable_dream_prefixes"></string-array>
 
     <!-- ComponentName of a dream to show whenever the system would otherwise have
          gone to sleep.  When the PowerManager is asked to go to sleep, it will instead
@@ -4967,6 +4969,10 @@
     <!-- If face auth sends the user directly to home/last open app, or stays on keyguard -->
     <bool name="config_faceAuthDismissesKeyguard">true</bool>
 
+    <!-- Default value for whether a SFPS device is required to be interactive for fingerprint auth
+    to unlock the device.  -->
+    <bool name="config_requireScreenOnToAuthEnabled">false</bool>
+
     <!-- The component name for the default profile supervisor, which can be set as a profile owner
     even after user setup is complete. The defined component should be used for supervision purposes
     only. The component must be part of a system app. -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 7a7b43a..44d8468 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2245,7 +2245,7 @@
   <java-symbol type="integer" name="config_dreamOverlayReconnectTimeoutMs" />
   <java-symbol type="integer" name="config_dreamOverlayMaxReconnectAttempts" />
   <java-symbol type="integer" name="config_minDreamOverlayDurationMs" />
-  <java-symbol type="string" name="config_loggable_dream_prefix" />
+  <java-symbol type="array" name="config_loggable_dream_prefixes" />
   <java-symbol type="string" name="config_dozeComponent" />
   <java-symbol type="string" name="enable_explore_by_touch_warning_title" />
   <java-symbol type="string" name="enable_explore_by_touch_warning_message" />
@@ -2723,6 +2723,7 @@
   <java-symbol type="array" name="config_face_acquire_vendor_biometricprompt_ignorelist" />
   <java-symbol type="bool" name="config_faceAuthSupportsSelfIllumination" />
   <java-symbol type="bool" name="config_faceAuthDismissesKeyguard" />
+  <java-symbol type="bool" name="config_requireScreenOnToAuthEnabled" />
 
   <!-- Face config -->
   <java-symbol type="integer" name="config_faceMaxTemplatesPerUser" />
@@ -4861,6 +4862,4 @@
   <java-symbol type="id" name="language_picker_header" />
 
   <java-symbol type="dimen" name="status_bar_height_default" />
-
-  <java-symbol type="bool" name="system_server_plays_face_haptics" />
 </resources>
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
index 1d513e4..16760e26 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java
@@ -1873,6 +1873,11 @@
         @Override
         public void onActivityPreCreated(@NonNull Activity activity,
                 @Nullable Bundle savedInstanceState) {
+            if (activity.isChild()) {
+                // Skip Activity that is child of another Activity (ActivityGroup) because it's
+                // window will just be a child of the parent Activity window.
+                return;
+            }
             synchronized (mLock) {
                 final IBinder activityToken = activity.getActivityToken();
                 final IBinder initialTaskFragmentToken =
@@ -1904,6 +1909,11 @@
         @Override
         public void onActivityPostCreated(@NonNull Activity activity,
                 @Nullable Bundle savedInstanceState) {
+            if (activity.isChild()) {
+                // Skip Activity that is child of another Activity (ActivityGroup) because it's
+                // window will just be a child of the parent Activity window.
+                return;
+            }
             // Calling after Activity#onCreate is complete to allow the app launch something
             // first. In case of a configured placeholder activity we want to make sure
             // that we don't launch it if an activity itself already requested something to be
@@ -1921,6 +1931,11 @@
 
         @Override
         public void onActivityConfigurationChanged(@NonNull Activity activity) {
+            if (activity.isChild()) {
+                // Skip Activity that is child of another Activity (ActivityGroup) because it's
+                // window will just be a child of the parent Activity window.
+                return;
+            }
             synchronized (mLock) {
                 final TransactionRecord transactionRecord = mTransactionManager
                         .startNewTransaction();
@@ -1934,6 +1949,11 @@
 
         @Override
         public void onActivityPostDestroyed(@NonNull Activity activity) {
+            if (activity.isChild()) {
+                // Skip Activity that is child of another Activity (ActivityGroup) because it's
+                // window will just be a child of the parent Activity window.
+                return;
+            }
             synchronized (mLock) {
                 SplitController.this.onActivityDestroyed(activity);
             }
@@ -1969,7 +1989,11 @@
             if (who instanceof Activity) {
                 // We will check if the new activity should be split with the activity that launched
                 // it.
-                launchingActivity = (Activity) who;
+                final Activity activity = (Activity) who;
+                // For Activity that is child of another Activity (ActivityGroup), treat the parent
+                // Activity as the launching one because it's window will just be a child of the
+                // parent Activity window.
+                launchingActivity = activity.isChild() ? activity.getParent() : activity;
                 if (isInPictureInPicture(launchingActivity)) {
                     // We don't embed activity when it is in PIP.
                     return super.onStartActivity(who, intent, options);
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index 7960323..362f1fa 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -227,7 +227,7 @@
         final TaskFragmentContainer curSecondaryContainer = mController.getContainerWithActivity(
                 secondaryActivity);
         TaskFragmentContainer containerToAvoid = primaryContainer;
-        if (curSecondaryContainer != null
+        if (curSecondaryContainer != null && curSecondaryContainer != primaryContainer
                 && (rule.shouldClearTop() || primaryContainer.isAbove(curSecondaryContainer))) {
             // Do not reuse the current TaskFragment if the rule is to clear top, or if it is below
             // the primary TaskFragment.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS
new file mode 100644
index 0000000..7237d2b
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/OWNERS
@@ -0,0 +1,2 @@
+# WM shell sub-modules splitscreen owner
+chenghsiuchang@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
index 616d447..d28a9f3 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipController.java
@@ -612,9 +612,24 @@
                 new DisplayInsetsController.OnInsetsChangedListener() {
                     @Override
                     public void insetsChanged(InsetsState insetsState) {
+                        int oldMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
                         onDisplayChanged(
                                 mDisplayController.getDisplayLayout(mPipBoundsState.getDisplayId()),
                                 false /* saveRestoreSnapFraction */);
+                        int newMaxMovementBound = mPipBoundsState.getMovementBounds().bottom;
+                        if (!mEnablePipKeepClearAlgorithm) {
+                            int pipTop = mPipBoundsState.getBounds().top;
+                            int diff = newMaxMovementBound - oldMaxMovementBound;
+                            if (diff < 0 && pipTop > newMaxMovementBound) {
+                                // bottom inset has increased, move PiP up if it is too low
+                                mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(),
+                                        newMaxMovementBound - pipTop);
+                            }
+                            if (diff > 0 && oldMaxMovementBound == pipTop) {
+                                // bottom inset has decreased, move PiP down if it was by the edge
+                                mPipMotionHelper.animateToOffset(mPipBoundsState.getBounds(), diff);
+                            }
+                        }
                     }
                 });
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
index 975d4bb..a9a97be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java
@@ -427,7 +427,7 @@
         // If this is from an IME or shelf adjustment, then we should move the PiP so that it is not
         // occluded by the IME or shelf.
         if (fromImeAdjustment || fromShelfAdjustment) {
-            if (mTouchState.isUserInteracting()) {
+            if (mTouchState.isUserInteracting() && mTouchState.isDragging()) {
                 // Defer the update of the current movement bounds until after the user finishes
                 // touching the screen
             } else if (ENABLE_PIP_KEEP_CLEAR_ALGORITHM) {
diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS
index f4efc37..1c28c3d 100644
--- a/libs/WindowManager/Shell/tests/OWNERS
+++ b/libs/WindowManager/Shell/tests/OWNERS
@@ -6,3 +6,4 @@
 lbill@google.com
 madym@google.com
 hwwang@google.com
+chenghsiuchang@google.com
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
index 453a713..8946516 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java
@@ -122,6 +122,7 @@
         Settings.Secure.FINGERPRINT_SIDE_FPS_BP_POWER_WINDOW,
         Settings.Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
         Settings.Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME,
+        Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED,
         Settings.Secure.ACTIVE_UNLOCK_ON_WAKE,
         Settings.Secure.ACTIVE_UNLOCK_ON_UNLOCK_INTENT,
         Settings.Secure.ACTIVE_UNLOCK_ON_BIOMETRIC_FAIL,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
index a39735f..cbf7953 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java
@@ -177,6 +177,7 @@
         VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_ENROLL_TAP_WINDOW,
                 NON_NEGATIVE_INTEGER_VALIDATOR);
         VALIDATORS.put(Secure.FINGERPRINT_SIDE_FPS_AUTH_DOWNTIME, NON_NEGATIVE_INTEGER_VALIDATOR);
+        VALIDATORS.put(Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.SHOW_MEDIA_WHEN_BYPASSING, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_APP_ENABLED, BOOLEAN_VALIDATOR);
         VALIDATORS.put(Secure.FACE_UNLOCK_ALWAYS_REQUIRE_CONFIRMATION, BOOLEAN_VALIDATOR);
diff --git a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
index 006b260..9add32c 100644
--- a/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
+++ b/packages/SystemUI/res/layout/dream_overlay_status_bar_view.xml
@@ -18,6 +18,7 @@
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     android:id="@+id/dream_overlay_status_bar"
+    android:visibility="invisible"
     android:layout_width="match_parent"
     android:layout_height="@dimen/dream_overlay_status_bar_height"
     android:paddingEnd="@dimen/dream_overlay_status_bar_margin"
diff --git a/packages/SystemUI/res/values-sw600dp-h900dp/dimens.xml b/packages/SystemUI/res/values-sw600dp-h900dp/dimens.xml
new file mode 100644
index 0000000..aab914f
--- /dev/null
+++ b/packages/SystemUI/res/values-sw600dp-h900dp/dimens.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2022 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<!-- Intended for wide devices that are currently oriented with a lot of available height,
+     such as tablets. 'hxxxdp' is used instead of 'port' in order to avoid this being applied
+     to wide devices that are shorter in height, like foldables. -->
+<resources>
+    <!-- Space between status view and notification shelf -->
+    <dimen name="keyguard_status_view_bottom_margin">35dp</dimen>
+    <dimen name="keyguard_clock_top_margin">40dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values-sw600dp-port/dimens.xml b/packages/SystemUI/res/values-sw600dp-port/dimens.xml
index 347cf29..d9df337 100644
--- a/packages/SystemUI/res/values-sw600dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-port/dimens.xml
@@ -17,8 +17,6 @@
 <resources>
     <dimen name="notification_panel_margin_horizontal">48dp</dimen>
     <dimen name="status_view_margin_horizontal">62dp</dimen>
-    <dimen name="keyguard_clock_top_margin">40dp</dimen>
-    <dimen name="keyguard_status_view_bottom_margin">40dp</dimen>
     <dimen name="bouncer_user_switcher_y_trans">20dp</dimen>
 
     <!-- qs_tiles_page_horizontal_margin should be margin / 2, otherwise full space between two
diff --git a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
new file mode 100644
index 0000000..97ead01
--- /dev/null
+++ b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml
@@ -0,0 +1,24 @@
+<?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.
+  -->
+<!-- Intended for wide devices that are currently oriented with a lot of available height,
+     such as tablets. 'hxxxdp' is used instead of 'port' in order to avoid this being applied
+     to wide devices that are shorter in height, like foldables. -->
+<resources>
+    <!-- Space between status view and notification shelf -->
+    <dimen name="keyguard_status_view_bottom_margin">70dp</dimen>
+    <dimen name="keyguard_clock_top_margin">80dp</dimen>
+</resources>
diff --git a/packages/SystemUI/res/values-sw720dp-port/dimens.xml b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
index 3d8da8a..17f82b5 100644
--- a/packages/SystemUI/res/values-sw720dp-port/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-port/dimens.xml
@@ -21,8 +21,6 @@
      for different hardware and product builds. -->
 <resources>
     <dimen name="status_view_margin_horizontal">124dp</dimen>
-    <dimen name="keyguard_clock_top_margin">80dp</dimen>
-    <dimen name="keyguard_status_view_bottom_margin">80dp</dimen>
     <dimen name="bouncer_user_switcher_y_trans">200dp</dimen>
 
     <dimen name="large_screen_shade_header_left_padding">24dp</dimen>
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index 93982cb..ce9829b 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -743,6 +743,17 @@
 
     <integer name="complicationRestoreMs">1000</integer>
 
+    <!-- Duration in milliseconds of the dream in un-blur animation. -->
+    <integer name="config_dreamOverlayInBlurDurationMs">249</integer>
+    <!-- Delay in milliseconds of the dream in un-blur animation. -->
+    <integer name="config_dreamOverlayInBlurDelayMs">133</integer>
+    <!-- Duration in milliseconds of the dream in complications fade-in animation. -->
+    <integer name="config_dreamOverlayInComplicationsDurationMs">282</integer>
+    <!-- Delay in milliseconds of the dream in top complications fade-in animation. -->
+    <integer name="config_dreamOverlayInTopComplicationsDelayMs">216</integer>
+    <!-- Delay in milliseconds of the dream in bottom complications fade-in animation. -->
+    <integer name="config_dreamOverlayInBottomComplicationsDelayMs">299</integer>
+
     <!-- Icons that don't show in a collapsed non-keyguard statusbar -->
     <string-array name="config_collapsed_statusbar_icon_blocklist" translatable="false">
         <item>@*android:string/status_bar_volume</item>
@@ -783,4 +794,8 @@
         <item>@color/dream_overlay_aqi_very_unhealthy</item>
         <item>@color/dream_overlay_aqi_hazardous</item>
     </integer-array>
+
+    <!-- Whether the device should display hotspot UI. If true, UI will display only when tethering
+         is available. If false, UI will never show regardless of tethering availability" -->
+    <bool name="config_show_wifi_tethering">true</bool>
 </resources>
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt
new file mode 100644
index 0000000..2dc7a28
--- /dev/null
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/keyguard/shared/model/KeyguardQuickAffordanceSlots.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.shared.keyguard.shared.model
+
+/**
+ * Collection of all supported "slots", placements where keyguard quick affordances can appear on
+ * the lock screen.
+ */
+object KeyguardQuickAffordanceSlots {
+    const val SLOT_ID_BOTTOM_START = "bottom_start"
+    const val SLOT_ID_BOTTOM_END = "bottom_end"
+}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
index e743ec8..bfbe88c 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/IOverviewProxy.aidl
@@ -20,6 +20,7 @@
 import android.graphics.Region;
 import android.os.Bundle;
 import android.view.MotionEvent;
+import android.view.SurfaceControl;
 import com.android.systemui.shared.recents.ISystemUiProxy;
 
 oneway interface IOverviewProxy {
@@ -44,12 +45,6 @@
     void onOverviewHidden(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) = 8;
 
     /**
-     * Sent when there was an action on one of the onboarding tips view.
-     * TODO: Move this implementation to SystemUI completely
-     */
-    void onTip(int actionType, int viewType) = 10;
-
-    /**
      * Sent when device assistant changes its default assistant whether it is available or not.
      */
     void onAssistantAvailable(boolean available) = 13;
@@ -60,13 +55,6 @@
     void onAssistantVisibilityChanged(float visibility) = 14;
 
     /**
-     * Sent when back is triggered.
-     * TODO: Move this implementation to SystemUI completely
-     */
-    void onBackAction(boolean completed, int downX, int downY, boolean isButton,
-            boolean gestureSwipeLeft) = 15;
-
-    /**
      * Sent when some system ui state changes.
      */
     void onSystemUiStateChanged(int stateFlags) = 16;
@@ -115,4 +103,9 @@
       * Sent when split keyboard shortcut is triggered to enter stage split.
       */
      void enterStageSplitFromRunningApp(boolean leftOrTop) = 25;
+
+     /**
+      * Sent when the surface for navigation bar is created or changed
+      */
+     void onNavigationBarSurface(in SurfaceControl surface) = 26;
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
index 2b2b05ce..b99b72b 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/recents/ISystemUiProxy.aidl
@@ -106,9 +106,6 @@
     /** Sets home rotation enabled. */
     void setHomeRotationEnabled(boolean enabled) = 45;
 
-    /** Notifies that a swipe-up gesture has started */
-    oneway void notifySwipeUpGestureStarted() = 46;
-
     /** Notifies when taskbar status updated */
     oneway void notifyTaskbarStatus(boolean visible, boolean stashed) = 47;
 
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index d48d7ff..c9b8712 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -228,7 +228,7 @@
                 listenForDozing(this)
                 if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
                     listenForDozeAmountTransition(this)
-                    listenForGoneToAodTransition(this)
+                    listenForAnyStateToAodTransition(this)
                 } else {
                     listenForDozeAmount(this)
                 }
@@ -286,10 +286,10 @@
      * dozing.
      */
     @VisibleForTesting
-    internal fun listenForGoneToAodTransition(scope: CoroutineScope): Job {
+    internal fun listenForAnyStateToAodTransition(scope: CoroutineScope): Job {
         return scope.launch {
-            keyguardTransitionInteractor.goneToAodTransition.filter {
-                it.transitionState == TransitionState.STARTED
+            keyguardTransitionInteractor.anyStateToAodTransition.filter {
+                it.transitionState == TransitionState.FINISHED
             }.collect {
                 dozeAmount = 1f
                 clock?.animations?.doze(dozeAmount)
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
index 71470e8..a0206f1 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardListenModel.kt
@@ -35,6 +35,7 @@
     val keyguardOccluded: Boolean,
     val occludingAppRequestingFp: Boolean,
     val primaryUser: Boolean,
+    val shouldListenSfpsState: Boolean,
     val shouldListenForFingerprintAssistant: Boolean,
     val switchingUser: Boolean,
     val udfps: Boolean,
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
index c756a17..2bb3a5f 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainer.java
@@ -106,8 +106,9 @@
     static final int USER_TYPE_WORK_PROFILE = 2;
     static final int USER_TYPE_SECONDARY_USER = 3;
 
-    @IntDef({MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER})
+    @IntDef({MODE_UNINITIALIZED, MODE_DEFAULT, MODE_ONE_HANDED, MODE_USER_SWITCHER})
     public @interface Mode {}
+    static final int MODE_UNINITIALIZED = -1;
     static final int MODE_DEFAULT = 0;
     static final int MODE_ONE_HANDED = 1;
     static final int MODE_USER_SWITCHER = 2;
@@ -154,7 +155,11 @@
     private boolean mDisappearAnimRunning;
     private SwipeListener mSwipeListener;
     private ViewMode mViewMode = new DefaultViewMode();
-    private @Mode int mCurrentMode = MODE_DEFAULT;
+    /*
+     * Using MODE_UNINITIALIZED to mean the view mode is set to DefaultViewMode, but init() has not
+     * yet been called on it. This will happen when the ViewController is initialized.
+     */
+    private @Mode int mCurrentMode = MODE_UNINITIALIZED;
     private int mWidth = -1;
 
     private final WindowInsetsAnimation.Callback mWindowInsetsAnimationCallback =
@@ -347,6 +352,8 @@
 
     private String modeToString(@Mode int mode) {
         switch (mode) {
+            case MODE_UNINITIALIZED:
+                return "Uninitialized";
             case MODE_DEFAULT:
                 return "Default";
             case MODE_ONE_HANDED:
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
index 79a01b9..fbb114c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -318,6 +318,7 @@
     @Override
     public void onInit() {
         mSecurityViewFlipperController.init();
+        configureMode();
     }
 
     @Override
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index bad75e8..558869c 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -151,6 +151,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.util.Assert;
+import com.android.systemui.util.settings.SecureSettings;
 
 import com.google.android.collect.Lists;
 
@@ -337,17 +338,20 @@
     private final ArrayList<WeakReference<KeyguardUpdateMonitorCallback>>
             mCallbacks = Lists.newArrayList();
     private ContentObserver mDeviceProvisionedObserver;
+    private ContentObserver mSfpsRequireScreenOnToAuthPrefObserver;
     private final ContentObserver mTimeFormatChangeObserver;
 
     private boolean mSwitchingUser;
 
     private boolean mDeviceInteractive;
+    private boolean mSfpsRequireScreenOnToAuthPrefEnabled;
     private final SubscriptionManager mSubscriptionManager;
     private final TelephonyListenerManager mTelephonyListenerManager;
     private final TrustManager mTrustManager;
     private final UserManager mUserManager;
     private final DevicePolicyManager mDevicePolicyManager;
     private final BroadcastDispatcher mBroadcastDispatcher;
+    private final SecureSettings mSecureSettings;
     private final InteractionJankMonitor mInteractionJankMonitor;
     private final LatencyTracker mLatencyTracker;
     private final StatusBarStateController mStatusBarStateController;
@@ -396,6 +400,7 @@
     protected Handler getHandler() {
         return mHandler;
     }
+
     private final Handler mHandler;
 
     private final IBiometricEnabledOnKeyguardCallback mBiometricEnabledCallback =
@@ -720,6 +725,7 @@
 
     /**
      * Request to listen for face authentication when an app is occluding keyguard.
+     *
      * @param request if true and mKeyguardOccluded, request face auth listening, else default
      *                to normal behavior.
      *                See {@link KeyguardUpdateMonitor#shouldListenForFace()}
@@ -732,6 +738,7 @@
 
     /**
      * Request to listen for fingerprint when an app is occluding keyguard.
+     *
      * @param request if true and mKeyguardOccluded, request fingerprint listening, else default
      *                to normal behavior.
      *                See {@link KeyguardUpdateMonitor#shouldListenForFingerprint(boolean)}
@@ -1946,6 +1953,7 @@
             Context context,
             @Main Looper mainLooper,
             BroadcastDispatcher broadcastDispatcher,
+            SecureSettings secureSettings,
             DumpManager dumpManager,
             @Background Executor backgroundExecutor,
             @Main Executor mainExecutor,
@@ -1988,6 +1996,7 @@
         mStatusBarState = mStatusBarStateController.getState();
         mLockPatternUtils = lockPatternUtils;
         mAuthController = authController;
+        mSecureSettings = secureSettings;
         dumpManager.registerDumpable(getClass().getName(), this);
         mSensorPrivacyManager = sensorPrivacyManager;
         mActiveUnlockConfig = activeUnlockConfiguration;
@@ -2229,9 +2238,35 @@
                                 Settings.System.TIME_12_24)));
             }
         };
+
         mContext.getContentResolver().registerContentObserver(
                 Settings.System.getUriFor(Settings.System.TIME_12_24),
                 false, mTimeFormatChangeObserver, UserHandle.USER_ALL);
+
+        updateSfpsRequireScreenOnToAuthPref();
+        mSfpsRequireScreenOnToAuthPrefObserver = new ContentObserver(mHandler) {
+            @Override
+            public void onChange(boolean selfChange) {
+                updateSfpsRequireScreenOnToAuthPref();
+            }
+        };
+
+        mContext.getContentResolver().registerContentObserver(
+                mSecureSettings.getUriFor(
+                        Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED),
+                false,
+                mSfpsRequireScreenOnToAuthPrefObserver,
+                getCurrentUser());
+    }
+
+    protected void updateSfpsRequireScreenOnToAuthPref() {
+        final int defaultSfpsRequireScreenOnToAuthValue =
+                mContext.getResources().getBoolean(
+                        com.android.internal.R.bool.config_requireScreenOnToAuthEnabled) ? 1 : 0;
+        mSfpsRequireScreenOnToAuthPrefEnabled = mSecureSettings.getIntForUser(
+                Settings.Secure.SFPS_REQUIRE_SCREEN_ON_TO_AUTH_ENABLED,
+                defaultSfpsRequireScreenOnToAuthValue,
+                getCurrentUser()) != 0;
     }
 
     private void initializeSimState() {
@@ -2276,6 +2311,22 @@
     }
 
     /**
+     * @return true if there's at least one sfps enrollment for the current user.
+     */
+    public boolean isSfpsEnrolled() {
+        return mAuthController.isSfpsEnrolled(getCurrentUser());
+    }
+
+    /**
+     * @return true if sfps HW is supported on this device. Can return true even if the user has
+     * not enrolled sfps. This may be false if called before onAllAuthenticatorsRegistered.
+     */
+    public boolean isSfpsSupported() {
+        return mAuthController.getSfpsProps() != null
+                && !mAuthController.getSfpsProps().isEmpty();
+    }
+
+    /**
      * @return true if there's at least one face enrolled
      */
     public boolean isFaceEnrolled() {
@@ -2598,13 +2649,21 @@
                 !(mFingerprintLockedOut && mBouncerIsOrWillBeShowing && mCredentialAttempted);
 
         final boolean isEncryptedOrLockdownForUser = isEncryptedOrLockdown(user);
+
         final boolean shouldListenUdfpsState = !isUdfps
                 || (!userCanSkipBouncer
-                    && !isEncryptedOrLockdownForUser
-                    && userDoesNotHaveTrust);
+                && !isEncryptedOrLockdownForUser
+                && userDoesNotHaveTrust);
 
-        final boolean shouldListen = shouldListenKeyguardState && shouldListenUserState
-                && shouldListenBouncerState && shouldListenUdfpsState && !isFingerprintLockedOut();
+        boolean shouldListenSideFpsState = true;
+        if (isSfpsSupported() && isSfpsEnrolled()) {
+            shouldListenSideFpsState =
+                    mSfpsRequireScreenOnToAuthPrefEnabled ? isDeviceInteractive() : true;
+        }
+
+        boolean shouldListen = shouldListenKeyguardState && shouldListenUserState
+                && shouldListenBouncerState && shouldListenUdfpsState && !isFingerprintLockedOut()
+                && shouldListenSideFpsState;
 
         maybeLogListenerModelData(
                 new KeyguardFingerprintListenModel(
@@ -2626,6 +2685,7 @@
                     mKeyguardOccluded,
                     mOccludingAppRequestingFp,
                     mIsPrimaryUser,
+                    shouldListenSideFpsState,
                     shouldListenForFingerprintAssistant,
                     mSwitchingUser,
                     isUdfps,
@@ -3727,6 +3787,11 @@
             mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver);
         }
 
+        if (mSfpsRequireScreenOnToAuthPrefObserver != null) {
+            mContext.getContentResolver().unregisterContentObserver(
+                    mSfpsRequireScreenOnToAuthPrefObserver);
+        }
+
         try {
             ActivityManager.getService().unregisterUserSwitchObserver(mUserSwitchObserver);
         } catch (RemoteException e) {
@@ -3799,6 +3864,11 @@
                 pw.println("        mBouncerIsOrWillBeShowing=" + mBouncerIsOrWillBeShowing);
                 pw.println("        mStatusBarState=" + StatusBarState.toString(mStatusBarState));
                 pw.println("        mUdfpsBouncerShowing=" + mUdfpsBouncerShowing);
+            } else if (isSfpsSupported()) {
+                pw.println("        sfpsEnrolled=" + isSfpsEnrolled());
+                pw.println("        shouldListenForSfps=" + shouldListenForFingerprint(false));
+                pw.println("        mSfpsRequireScreenOnToAuthPrefEnabled="
+                        + mSfpsRequireScreenOnToAuthPrefEnabled);
             }
         }
         if (mFaceManager != null && mFaceManager.isHardwareDetected()) {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 37da2c7..0a2d8ec 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -762,6 +762,12 @@
         }
         mContainerState = STATE_ANIMATING_OUT;
 
+        // Request hiding soft-keyboard before animating away credential UI, in case IME insets
+        // animation get delayed by dismissing animation.
+        if (isAttachedToWindow() && getRootWindowInsets().isVisible(WindowInsets.Type.ime())) {
+            getWindowInsetsController().hide(WindowInsets.Type.ime());
+        }
+
         if (sendReason) {
             mPendingCallbackReason = reason;
         } else {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index c015a21..ff18eee 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -78,6 +78,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.Execution;
 
@@ -150,6 +151,7 @@
     @Nullable private List<FingerprintSensorPropertiesInternal> mSidefpsProps;
 
     @NonNull private final SparseBooleanArray mUdfpsEnrolledForUser;
+    @NonNull private final SparseBooleanArray mSfpsEnrolledForUser;
     @NonNull private final SensorPrivacyManager mSensorPrivacyManager;
     private final WakefulnessLifecycle mWakefulnessLifecycle;
     private boolean mAllFingerprintAuthenticatorsRegistered;
@@ -159,6 +161,19 @@
     private final @Background DelayableExecutor mBackgroundExecutor;
     private final DisplayInfo mCachedDisplayInfo = new DisplayInfo();
 
+
+    private final VibratorHelper mVibratorHelper;
+
+    private void vibrateSuccess(int modality) {
+        mVibratorHelper.vibrateAuthSuccess(
+                getClass().getSimpleName() + ", modality = " + modality + "BP::success");
+    }
+
+    private void vibrateError(int modality) {
+        mVibratorHelper.vibrateAuthError(
+                getClass().getSimpleName() + ", modality = " + modality + "BP::error");
+    }
+
     @VisibleForTesting
     final TaskStackListener mTaskStackListener = new TaskStackListener() {
         @Override
@@ -325,6 +340,16 @@
                 }
             }
         }
+
+        if (mSidefpsProps == null) {
+            Log.d(TAG, "handleEnrollmentsChanged, mSidefpsProps is null");
+        } else {
+            for (FingerprintSensorPropertiesInternal prop : mSidefpsProps) {
+                if (prop.sensorId == sensorId) {
+                    mSfpsEnrolledForUser.put(userId, hasEnrollments);
+                }
+            }
+        }
         for (Callback cb : mCallbacks) {
             cb.onEnrollmentsChanged();
         }
@@ -660,7 +685,8 @@
             @NonNull StatusBarStateController statusBarStateController,
             @NonNull InteractionJankMonitor jankMonitor,
             @Main Handler handler,
-            @Background DelayableExecutor bgExecutor) {
+            @Background DelayableExecutor bgExecutor,
+            @NonNull VibratorHelper vibrator) {
         mContext = context;
         mExecution = execution;
         mUserManager = userManager;
@@ -677,6 +703,8 @@
         mWindowManager = windowManager;
         mInteractionJankMonitor = jankMonitor;
         mUdfpsEnrolledForUser = new SparseBooleanArray();
+        mSfpsEnrolledForUser = new SparseBooleanArray();
+        mVibratorHelper = vibrator;
 
         mOrientationListener = new BiometricDisplayListener(
                 context,
@@ -866,6 +894,8 @@
     public void onBiometricAuthenticated(@Modality int modality) {
         if (DEBUG) Log.d(TAG, "onBiometricAuthenticated: ");
 
+        vibrateSuccess(modality);
+
         if (mCurrentDialog != null) {
             mCurrentDialog.onAuthenticationSucceeded(modality);
         } else {
@@ -889,6 +919,11 @@
         return mUdfpsProps;
     }
 
+    @Nullable
+    public List<FingerprintSensorPropertiesInternal> getSfpsProps() {
+        return mSidefpsProps;
+    }
+
     private String getErrorString(@Modality int modality, int error, int vendorCode) {
         switch (modality) {
             case TYPE_FACE:
@@ -913,6 +948,8 @@
             Log.d(TAG, String.format("onBiometricError(%d, %d, %d)", modality, error, vendorCode));
         }
 
+        vibrateError(modality);
+
         final boolean isLockout = (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT)
                 || (error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT);
 
@@ -1013,6 +1050,17 @@
         return mUdfpsEnrolledForUser.get(userId);
     }
 
+    /**
+     * Whether the passed userId has enrolled SFPS.
+     */
+    public boolean isSfpsEnrolled(int userId) {
+        if (mSidefpsController == null) {
+            return false;
+        }
+
+        return mSfpsEnrolledForUser.get(userId);
+    }
+
     private void showDialog(SomeArgs args, boolean skipAnimation, Bundle savedState) {
         mCurrentDialogArgs = args;
 
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 96fe65f..65fcd76 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -60,7 +60,9 @@
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.doze.DozeReceiver;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -119,6 +121,7 @@
     @NonNull private final SystemUIDialogManager mDialogManager;
     @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
     @NonNull private final VibratorHelper mVibrator;
+    @NonNull private final FeatureFlags mFeatureFlags;
     @NonNull private final FalsingManager mFalsingManager;
     @NonNull private final PowerManager mPowerManager;
     @NonNull private final AccessibilityManager mAccessibilityManager;
@@ -130,6 +133,7 @@
     @NonNull private final LatencyTracker mLatencyTracker;
     @VisibleForTesting @NonNull final BiometricDisplayListener mOrientationListener;
     @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator;
+    @NonNull private final BouncerInteractor mBouncerInteractor;
 
     // Currently the UdfpsController supports a single UDFPS sensor. If devices have multiple
     // sensors, this, in addition to a lot of the code here, will be updated.
@@ -212,7 +216,8 @@
                             mUnlockedScreenOffAnimationController,
                             mUdfpsDisplayMode, requestId, reason, callback,
                             (view, event, fromUdfpsView) -> onTouch(requestId, event,
-                                    fromUdfpsView), mActivityLaunchAnimator)));
+                                    fromUdfpsView), mActivityLaunchAnimator, mFeatureFlags,
+                            mBouncerInteractor)));
         }
 
         @Override
@@ -590,6 +595,7 @@
             @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
             @NonNull DumpManager dumpManager,
             @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
+            @NonNull FeatureFlags featureFlags,
             @NonNull FalsingManager falsingManager,
             @NonNull PowerManager powerManager,
             @NonNull AccessibilityManager accessibilityManager,
@@ -608,7 +614,8 @@
             @NonNull LatencyTracker latencyTracker,
             @NonNull ActivityLaunchAnimator activityLaunchAnimator,
             @NonNull Optional<AlternateUdfpsTouchProvider> alternateTouchProvider,
-            @BiometricsBackground Executor biometricsExecutor) {
+            @BiometricsBackground Executor biometricsExecutor,
+            @NonNull BouncerInteractor bouncerInteractor) {
         mContext = context;
         mExecution = execution;
         mVibrator = vibrator;
@@ -638,6 +645,8 @@
         mActivityLaunchAnimator = activityLaunchAnimator;
         mAlternateTouchProvider = alternateTouchProvider.orElse(null);
         mBiometricExecutor = biometricsExecutor;
+        mFeatureFlags = featureFlags;
+        mBouncerInteractor = bouncerInteractor;
 
         mOrientationListener = new BiometricDisplayListener(
                 context,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 7d01096..d70861a 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -48,6 +48,8 @@
 import com.android.systemui.R
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -70,29 +72,31 @@
  */
 @UiThread
 class UdfpsControllerOverlay @JvmOverloads constructor(
-    private val context: Context,
-    fingerprintManager: FingerprintManager,
-    private val inflater: LayoutInflater,
-    private val windowManager: WindowManager,
-    private val accessibilityManager: AccessibilityManager,
-    private val statusBarStateController: StatusBarStateController,
-    private val shadeExpansionStateManager: ShadeExpansionStateManager,
-    private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
-    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
-    private val dialogManager: SystemUIDialogManager,
-    private val dumpManager: DumpManager,
-    private val transitionController: LockscreenShadeTransitionController,
-    private val configurationController: ConfigurationController,
-    private val systemClock: SystemClock,
-    private val keyguardStateController: KeyguardStateController,
-    private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
-    private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
-    val requestId: Long,
-    @ShowReason val requestReason: Int,
-    private val controllerCallback: IUdfpsOverlayControllerCallback,
-    private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
-    private val activityLaunchAnimator: ActivityLaunchAnimator,
-    private val isDebuggable: Boolean = Build.IS_DEBUGGABLE
+        private val context: Context,
+        fingerprintManager: FingerprintManager,
+        private val inflater: LayoutInflater,
+        private val windowManager: WindowManager,
+        private val accessibilityManager: AccessibilityManager,
+        private val statusBarStateController: StatusBarStateController,
+        private val shadeExpansionStateManager: ShadeExpansionStateManager,
+        private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
+        private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+        private val dialogManager: SystemUIDialogManager,
+        private val dumpManager: DumpManager,
+        private val transitionController: LockscreenShadeTransitionController,
+        private val configurationController: ConfigurationController,
+        private val systemClock: SystemClock,
+        private val keyguardStateController: KeyguardStateController,
+        private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
+        private var udfpsDisplayModeProvider: UdfpsDisplayModeProvider,
+        val requestId: Long,
+        @ShowReason val requestReason: Int,
+        private val controllerCallback: IUdfpsOverlayControllerCallback,
+        private val onTouch: (View, MotionEvent, Boolean) -> Boolean,
+        private val activityLaunchAnimator: ActivityLaunchAnimator,
+        private val featureFlags: FeatureFlags,
+        private val bouncerInteractor: BouncerInteractor,
+        private val isDebuggable: Boolean = Build.IS_DEBUGGABLE
 ) {
     /** The view, when [isShowing], or null. */
     var overlayView: UdfpsView? = null
@@ -246,7 +250,9 @@
                     unlockedScreenOffAnimationController,
                     dialogManager,
                     controller,
-                    activityLaunchAnimator
+                    activityLaunchAnimator,
+                    featureFlags,
+                    bouncerInteractor
                 )
             }
             REASON_AUTH_BP -> {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
deleted file mode 100644
index 4d7f89d..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.java
+++ /dev/null
@@ -1,548 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics;
-
-import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
-
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.content.res.Configuration;
-import android.util.MathUtils;
-import android.view.MotionEvent;
-
-import com.android.keyguard.BouncerPanelExpansionCalculator;
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.R;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.animation.Interpolators;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shade.ShadeExpansionChangeEvent;
-import com.android.systemui.shade.ShadeExpansionListener;
-import com.android.systemui.shade.ShadeExpansionStateManager;
-import com.android.systemui.statusbar.LockscreenShadeTransitionController;
-import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.stack.StackStateAnimator;
-import com.android.systemui.statusbar.phone.KeyguardBouncer;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
-import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.time.SystemClock;
-
-import java.io.PrintWriter;
-
-/**
- * Class that coordinates non-HBM animations during keyguard authentication.
- */
-public class UdfpsKeyguardViewController extends UdfpsAnimationViewController<UdfpsKeyguardView> {
-    public static final String TAG = "UdfpsKeyguardViewCtrl";
-    @NonNull private final StatusBarKeyguardViewManager mKeyguardViewManager;
-    @NonNull private final KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @NonNull private final LockscreenShadeTransitionController mLockScreenShadeTransitionController;
-    @NonNull private final ConfigurationController mConfigurationController;
-    @NonNull private final SystemClock mSystemClock;
-    @NonNull private final KeyguardStateController mKeyguardStateController;
-    @NonNull private final UdfpsController mUdfpsController;
-    @NonNull private final UnlockedScreenOffAnimationController
-            mUnlockedScreenOffAnimationController;
-    @NonNull private final ActivityLaunchAnimator mActivityLaunchAnimator;
-    private final ValueAnimator mUnlockedScreenOffDozeAnimator = ValueAnimator.ofFloat(0f, 1f);
-
-    private boolean mShowingUdfpsBouncer;
-    private boolean mUdfpsRequested;
-    private float mQsExpansion;
-    private boolean mFaceDetectRunning;
-    private int mStatusBarState;
-    private float mTransitionToFullShadeProgress;
-    private float mLastDozeAmount;
-    private long mLastUdfpsBouncerShowTime = -1;
-    private float mPanelExpansionFraction;
-    private boolean mLaunchTransitionFadingAway;
-    private boolean mIsLaunchingActivity;
-    private float mActivityLaunchProgress;
-
-    /**
-     * hidden amount of pin/pattern/password bouncer
-     * {@link KeyguardBouncer#EXPANSION_VISIBLE} (0f) to
-     * {@link KeyguardBouncer#EXPANSION_HIDDEN} (1f)
-     */
-    private float mInputBouncerHiddenAmount;
-    private boolean mIsGenericBouncerShowing; // whether UDFPS bouncer or input bouncer is visible
-
-    protected UdfpsKeyguardViewController(
-            @NonNull UdfpsKeyguardView view,
-            @NonNull StatusBarStateController statusBarStateController,
-            @NonNull ShadeExpansionStateManager shadeExpansionStateManager,
-            @NonNull StatusBarKeyguardViewManager statusBarKeyguardViewManager,
-            @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor,
-            @NonNull DumpManager dumpManager,
-            @NonNull LockscreenShadeTransitionController transitionController,
-            @NonNull ConfigurationController configurationController,
-            @NonNull SystemClock systemClock,
-            @NonNull KeyguardStateController keyguardStateController,
-            @NonNull UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
-            @NonNull SystemUIDialogManager systemUIDialogManager,
-            @NonNull UdfpsController udfpsController,
-            @NonNull ActivityLaunchAnimator activityLaunchAnimator) {
-        super(view, statusBarStateController, shadeExpansionStateManager, systemUIDialogManager,
-                dumpManager);
-        mKeyguardViewManager = statusBarKeyguardViewManager;
-        mKeyguardUpdateMonitor = keyguardUpdateMonitor;
-        mLockScreenShadeTransitionController = transitionController;
-        mConfigurationController = configurationController;
-        mSystemClock = systemClock;
-        mKeyguardStateController = keyguardStateController;
-        mUdfpsController = udfpsController;
-        mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController;
-        mActivityLaunchAnimator = activityLaunchAnimator;
-
-        mUnlockedScreenOffDozeAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
-        mUnlockedScreenOffDozeAnimator.setInterpolator(Interpolators.ALPHA_IN);
-        mUnlockedScreenOffDozeAnimator.addUpdateListener(
-                new ValueAnimator.AnimatorUpdateListener() {
-                    @Override
-                    public void onAnimationUpdate(ValueAnimator animation) {
-                        mView.onDozeAmountChanged(
-                                animation.getAnimatedFraction(),
-                                (float) animation.getAnimatedValue(),
-                                UdfpsKeyguardView.ANIMATION_UNLOCKED_SCREEN_OFF);
-                    }
-                });
-    }
-
-    @Override
-    @NonNull protected String getTag() {
-        return "UdfpsKeyguardViewController";
-    }
-
-    @Override
-    public void onInit() {
-        super.onInit();
-        mKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
-    }
-
-    @Override
-    protected void onViewAttached() {
-        super.onViewAttached();
-        final float dozeAmount = getStatusBarStateController().getDozeAmount();
-        mLastDozeAmount = dozeAmount;
-        mStateListener.onDozeAmountChanged(dozeAmount, dozeAmount);
-        getStatusBarStateController().addCallback(mStateListener);
-
-        mUdfpsRequested = false;
-
-        mLaunchTransitionFadingAway = mKeyguardStateController.isLaunchTransitionFadingAway();
-        mKeyguardStateController.addCallback(mKeyguardStateControllerCallback);
-        mStatusBarState = getStatusBarStateController().getState();
-        mQsExpansion = mKeyguardViewManager.getQsExpansion();
-        updateGenericBouncerVisibility();
-        mConfigurationController.addCallback(mConfigurationListener);
-        getShadeExpansionStateManager().addExpansionListener(mShadeExpansionListener);
-        updateScaleFactor();
-        mView.updatePadding();
-        updateAlpha();
-        updatePauseAuth();
-
-        mKeyguardViewManager.setAlternateAuthInterceptor(mAlternateAuthInterceptor);
-        mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(this);
-        mActivityLaunchAnimator.addListener(mActivityLaunchAnimatorListener);
-    }
-
-    @Override
-    protected void onViewDetached() {
-        super.onViewDetached();
-        mFaceDetectRunning = false;
-
-        mKeyguardStateController.removeCallback(mKeyguardStateControllerCallback);
-        getStatusBarStateController().removeCallback(mStateListener);
-        mKeyguardViewManager.removeAlternateAuthInterceptor(mAlternateAuthInterceptor);
-        mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false);
-        mConfigurationController.removeCallback(mConfigurationListener);
-        getShadeExpansionStateManager().removeExpansionListener(mShadeExpansionListener);
-        if (mLockScreenShadeTransitionController.getUdfpsKeyguardViewController() == this) {
-            mLockScreenShadeTransitionController.setUdfpsKeyguardViewController(null);
-        }
-        mActivityLaunchAnimator.removeListener(mActivityLaunchAnimatorListener);
-    }
-
-    @Override
-    public void dump(PrintWriter pw, String[] args) {
-        super.dump(pw, args);
-        pw.println("mShowingUdfpsBouncer=" + mShowingUdfpsBouncer);
-        pw.println("mFaceDetectRunning=" + mFaceDetectRunning);
-        pw.println("mStatusBarState=" + StatusBarState.toString(mStatusBarState));
-        pw.println("mTransitionToFullShadeProgress=" + mTransitionToFullShadeProgress);
-        pw.println("mQsExpansion=" + mQsExpansion);
-        pw.println("mIsGenericBouncerShowing=" + mIsGenericBouncerShowing);
-        pw.println("mInputBouncerHiddenAmount=" + mInputBouncerHiddenAmount);
-        pw.println("mPanelExpansionFraction=" + mPanelExpansionFraction);
-        pw.println("unpausedAlpha=" + mView.getUnpausedAlpha());
-        pw.println("mUdfpsRequested=" + mUdfpsRequested);
-        pw.println("mLaunchTransitionFadingAway=" + mLaunchTransitionFadingAway);
-        pw.println("mLastDozeAmount=" + mLastDozeAmount);
-
-        mView.dump(pw);
-    }
-
-    /**
-     * Overrides non-bouncer show logic in shouldPauseAuth to still show icon.
-     * @return whether the udfpsBouncer has been newly shown or hidden
-     */
-    private boolean showUdfpsBouncer(boolean show) {
-        if (mShowingUdfpsBouncer == show) {
-            return false;
-        }
-
-        boolean udfpsAffordanceWasNotShowing = shouldPauseAuth();
-        mShowingUdfpsBouncer = show;
-        if (mShowingUdfpsBouncer) {
-            mLastUdfpsBouncerShowTime = mSystemClock.uptimeMillis();
-        }
-        if (mShowingUdfpsBouncer) {
-            if (udfpsAffordanceWasNotShowing) {
-                mView.animateInUdfpsBouncer(null);
-            }
-
-            if (mKeyguardStateController.isOccluded()) {
-                mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true);
-            }
-
-            mView.announceForAccessibility(mView.getContext().getString(
-                    R.string.accessibility_fingerprint_bouncer));
-        } else {
-            mKeyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false);
-        }
-
-        updateGenericBouncerVisibility();
-        updateAlpha();
-        updatePauseAuth();
-        return true;
-    }
-
-    /**
-     * Returns true if the fingerprint manager is running but we want to temporarily pause
-     * authentication. On the keyguard, we may want to show udfps when the shade
-     * is expanded, so this can be overridden with the showBouncer method.
-     */
-    public boolean shouldPauseAuth() {
-        if (mShowingUdfpsBouncer) {
-            return false;
-        }
-
-        if (mUdfpsRequested && !getNotificationShadeVisible()
-                && (!mIsGenericBouncerShowing
-                || mInputBouncerHiddenAmount != KeyguardBouncer.EXPANSION_VISIBLE)
-                && mKeyguardStateController.isShowing()) {
-            return false;
-        }
-
-        if (mLaunchTransitionFadingAway) {
-            return true;
-        }
-
-        // Only pause auth if we're not on the keyguard AND we're not transitioning to doze
-        // (ie: dozeAmount = 0f). For the UnlockedScreenOffAnimation, the statusBarState is
-        // delayed. However, we still animate in the UDFPS affordance with the 
-        // mUnlockedScreenOffDozeAnimator.
-        if (mStatusBarState != KEYGUARD && mLastDozeAmount == 0f) {
-            return true;
-        }
-
-        if (mInputBouncerHiddenAmount < .5f) {
-            return true;
-        }
-
-        if (mView.getUnpausedAlpha() < (255 * .1)) {
-            return true;
-        }
-
-        return false;
-    }
-
-    @Override
-    public boolean listenForTouchesOutsideView() {
-        return true;
-    }
-
-    @Override
-    public void onTouchOutsideView() {
-        maybeShowInputBouncer();
-    }
-
-    /**
-     * If we were previously showing the udfps bouncer, hide it and instead show the regular
-     * (pin/pattern/password) bouncer.
-     *
-     * Does nothing if we weren't previously showing the UDFPS bouncer.
-     */
-    private void maybeShowInputBouncer() {
-        if (mShowingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) {
-            mKeyguardViewManager.showBouncer(true);
-        }
-    }
-
-    /**
-     * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside
-     * of the udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password
-     * bouncer.
-     */
-    private boolean hasUdfpsBouncerShownWithMinTime() {
-        return (mSystemClock.uptimeMillis() - mLastUdfpsBouncerShowTime) > 200;
-    }
-
-    /**
-     * Set the progress we're currently transitioning to the full shade. 0.0f means we're not
-     * transitioning yet, while 1.0f means we've fully dragged down.
-     *
-     * For example, start swiping down to expand the notification shade from the empty space in
-     * the middle of the lock screen.
-     */
-    public void setTransitionToFullShadeProgress(float progress) {
-        mTransitionToFullShadeProgress = progress;
-        updateAlpha();
-    }
-
-    /**
-     * Update alpha for the UDFPS lock screen affordance. The AoD UDFPS visual affordance's
-     * alpha is based on the doze amount.
-     */
-    @Override
-    public void updateAlpha() {
-        // Fade icon on transitions to showing the status bar or bouncer, but if mUdfpsRequested,
-        // then the keyguard is occluded by some application - so instead use the input bouncer
-        // hidden amount to determine the fade.
-        float expansion = mUdfpsRequested ? mInputBouncerHiddenAmount : mPanelExpansionFraction;
-
-        int alpha = mShowingUdfpsBouncer ? 255
-                : (int) MathUtils.constrain(
-                    MathUtils.map(.5f, .9f, 0f, 255f, expansion),
-                    0f, 255f);
-
-        if (!mShowingUdfpsBouncer) {
-            // swipe from top of the lockscreen to expand full QS:
-            alpha *= (1.0f - Interpolators.EMPHASIZED_DECELERATE.getInterpolation(mQsExpansion));
-
-            // swipe from the middle (empty space) of lockscreen to expand the notification shade:
-            alpha *= (1.0f - mTransitionToFullShadeProgress);
-
-            // Fade out the icon if we are animating an activity launch over the lockscreen and the
-            // activity didn't request the UDFPS.
-            if (mIsLaunchingActivity && !mUdfpsRequested) {
-                alpha *= (1.0f - mActivityLaunchProgress);
-            }
-
-            // Fade out alpha when a dialog is shown
-            // Fade in alpha when a dialog is hidden
-            alpha *= mView.getDialogSuggestedAlpha();
-        }
-        mView.setUnpausedAlpha(alpha);
-    }
-
-    /**
-     * Updates mIsGenericBouncerShowing (whether any bouncer is showing) and updates the
-     * mInputBouncerHiddenAmount to reflect whether the input bouncer is fully showing or not.
-     */
-    private void updateGenericBouncerVisibility() {
-        mIsGenericBouncerShowing = mKeyguardViewManager.isBouncerShowing(); // includes altBouncer
-        final boolean altBouncerShowing = mKeyguardViewManager.isShowingAlternateAuth();
-        if (altBouncerShowing || !mKeyguardViewManager.bouncerIsOrWillBeShowing()) {
-            mInputBouncerHiddenAmount = 1f;
-        } else if (mIsGenericBouncerShowing) {
-            // input bouncer is fully showing
-            mInputBouncerHiddenAmount = 0f;
-        }
-    }
-
-    /**
-     * Update the scale factor based on the device's resolution.
-     */
-    private void updateScaleFactor() {
-        if (mUdfpsController != null && mUdfpsController.mOverlayParams != null) {
-            mView.setScaleFactor(mUdfpsController.mOverlayParams.getScaleFactor());
-        }
-    }
-
-    private final StatusBarStateController.StateListener mStateListener =
-            new StatusBarStateController.StateListener() {
-        @Override
-        public void onDozeAmountChanged(float linear, float eased) {
-            if (mLastDozeAmount < linear) {
-                showUdfpsBouncer(false);
-            }
-            mUnlockedScreenOffDozeAnimator.cancel();
-            final boolean animatingFromUnlockedScreenOff =
-                    mUnlockedScreenOffAnimationController.isAnimationPlaying();
-            if (animatingFromUnlockedScreenOff && linear != 0f) {
-                // we manually animate the fade in of the UDFPS icon since the unlocked
-                // screen off animation prevents the doze amounts to be incrementally eased in
-                mUnlockedScreenOffDozeAnimator.start();
-            } else {
-                mView.onDozeAmountChanged(linear, eased,
-                        UdfpsKeyguardView.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN);
-            }
-
-            mLastDozeAmount = linear;
-            updatePauseAuth();
-        }
-
-        @Override
-        public void onStateChanged(int statusBarState) {
-            mStatusBarState = statusBarState;
-            updateAlpha();
-            updatePauseAuth();
-        }
-    };
-
-    private final StatusBarKeyguardViewManager.AlternateAuthInterceptor mAlternateAuthInterceptor =
-            new StatusBarKeyguardViewManager.AlternateAuthInterceptor() {
-                @Override
-                public boolean showAlternateAuthBouncer() {
-                    return showUdfpsBouncer(true);
-                }
-
-                @Override
-                public boolean hideAlternateAuthBouncer() {
-                    return showUdfpsBouncer(false);
-                }
-
-                @Override
-                public boolean isShowingAlternateAuthBouncer() {
-                    return mShowingUdfpsBouncer;
-                }
-
-                @Override
-                public void requestUdfps(boolean request, int color) {
-                    mUdfpsRequested = request;
-                    mView.requestUdfps(request, color);
-                    updateAlpha();
-                    updatePauseAuth();
-                }
-
-                @Override
-                public boolean isAnimating() {
-                    return false;
-                }
-
-                /**
-                 * Set the amount qs is expanded. Forxample, swipe down from the top of the
-                 * lock screen to start the full QS expansion.
-                 */
-                @Override
-                public void setQsExpansion(float qsExpansion) {
-                    mQsExpansion = qsExpansion;
-                    updateAlpha();
-                    updatePauseAuth();
-                }
-
-                @Override
-                public boolean onTouch(MotionEvent event) {
-                    if (mTransitionToFullShadeProgress != 0) {
-                        return false;
-                    }
-                    return mUdfpsController.onTouch(event);
-                }
-
-                @Override
-                public void setBouncerExpansionChanged(float expansion) {
-                    mInputBouncerHiddenAmount = expansion;
-                    updateAlpha();
-                    updatePauseAuth();
-                }
-
-                /**
-                 * Only called on primary auth bouncer changes, not on whether the UDFPS bouncer
-                 * visibility changes.
-                 */
-                @Override
-                public void onBouncerVisibilityChanged() {
-                    updateGenericBouncerVisibility();
-                    updateAlpha();
-                    updatePauseAuth();
-                }
-
-                @Override
-                public void dump(PrintWriter pw) {
-                    pw.println(getTag());
-                }
-            };
-
-    private final ConfigurationController.ConfigurationListener mConfigurationListener =
-            new ConfigurationController.ConfigurationListener() {
-                @Override
-                public void onUiModeChanged() {
-                    mView.updateColor();
-                }
-
-                @Override
-                public void onThemeChanged() {
-                    mView.updateColor();
-                }
-
-                @Override
-                public void onConfigChanged(Configuration newConfig) {
-                    updateScaleFactor();
-                    mView.updatePadding();
-                    mView.updateColor();
-                }
-            };
-
-    private final ShadeExpansionListener mShadeExpansionListener = new ShadeExpansionListener() {
-        @Override
-        public void onPanelExpansionChanged(ShadeExpansionChangeEvent event) {
-            float fraction = event.getFraction();
-            mPanelExpansionFraction =
-                    mKeyguardViewManager.isBouncerInTransit() ? BouncerPanelExpansionCalculator
-                            .aboutToShowBouncerProgress(fraction) : fraction;
-            updateAlpha();
-            updatePauseAuth();
-        }
-    };
-
-    private final KeyguardStateController.Callback mKeyguardStateControllerCallback =
-            new KeyguardStateController.Callback() {
-                @Override
-                public void onLaunchTransitionFadingAwayChanged() {
-                    mLaunchTransitionFadingAway =
-                            mKeyguardStateController.isLaunchTransitionFadingAway();
-                    updatePauseAuth();
-                }
-            };
-
-    private final ActivityLaunchAnimator.Listener mActivityLaunchAnimatorListener =
-            new ActivityLaunchAnimator.Listener() {
-                @Override
-                public void onLaunchAnimationStart() {
-                    mIsLaunchingActivity = true;
-                    mActivityLaunchProgress = 0f;
-                    updateAlpha();
-                }
-
-                @Override
-                public void onLaunchAnimationEnd() {
-                    mIsLaunchingActivity = false;
-                    updateAlpha();
-                }
-
-                @Override
-                public void onLaunchAnimationProgress(float linearProgress) {
-                    mActivityLaunchProgress = linearProgress;
-                    updateAlpha();
-                }
-            };
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
new file mode 100644
index 0000000..5bae2dc
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsKeyguardViewController.kt
@@ -0,0 +1,550 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics
+
+import android.animation.ValueAnimator
+import android.content.res.Configuration
+import android.util.MathUtils
+import android.view.MotionEvent
+import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.systemui.R
+import com.android.systemui.animation.ActivityLaunchAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.shade.ShadeExpansionListener
+import com.android.systemui.shade.ShadeExpansionStateManager
+import com.android.systemui.statusbar.LockscreenShadeTransitionController
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.notification.stack.StackStateAnimator
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.statusbar.phone.KeyguardBouncer.BouncerExpansionCallback
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.AlternateAuthInterceptor
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager.KeyguardViewManagerCallback
+import com.android.systemui.statusbar.phone.SystemUIDialogManager
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController
+import com.android.systemui.statusbar.policy.ConfigurationController
+import com.android.systemui.statusbar.policy.KeyguardStateController
+import com.android.systemui.util.time.SystemClock
+import java.io.PrintWriter
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/** Class that coordinates non-HBM animations during keyguard authentication. */
+open class UdfpsKeyguardViewController
+constructor(
+    private val view: UdfpsKeyguardView,
+    statusBarStateController: StatusBarStateController,
+    shadeExpansionStateManager: ShadeExpansionStateManager,
+    private val keyguardViewManager: StatusBarKeyguardViewManager,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    dumpManager: DumpManager,
+    private val lockScreenShadeTransitionController: LockscreenShadeTransitionController,
+    private val configurationController: ConfigurationController,
+    private val systemClock: SystemClock,
+    private val keyguardStateController: KeyguardStateController,
+    private val unlockedScreenOffAnimationController: UnlockedScreenOffAnimationController,
+    systemUIDialogManager: SystemUIDialogManager,
+    private val udfpsController: UdfpsController,
+    private val activityLaunchAnimator: ActivityLaunchAnimator,
+    featureFlags: FeatureFlags,
+    private val bouncerInteractor: BouncerInteractor
+) :
+    UdfpsAnimationViewController<UdfpsKeyguardView>(
+        view,
+        statusBarStateController,
+        shadeExpansionStateManager,
+        systemUIDialogManager,
+        dumpManager
+    ) {
+    private val isModernBouncerEnabled: Boolean = featureFlags.isEnabled(Flags.MODERN_BOUNCER)
+    private var showingUdfpsBouncer = false
+    private var udfpsRequested = false
+    private var qsExpansion = 0f
+    private var faceDetectRunning = false
+    private var statusBarState = 0
+    private var transitionToFullShadeProgress = 0f
+    private var lastDozeAmount = 0f
+    private var lastUdfpsBouncerShowTime: Long = -1
+    private var panelExpansionFraction = 0f
+    private var launchTransitionFadingAway = false
+    private var isLaunchingActivity = false
+    private var activityLaunchProgress = 0f
+    private val unlockedScreenOffDozeAnimator =
+        ValueAnimator.ofFloat(0f, 1f).apply {
+            duration = StackStateAnimator.ANIMATION_DURATION_STANDARD.toLong()
+            interpolator = Interpolators.ALPHA_IN
+            addUpdateListener { animation ->
+                view.onDozeAmountChanged(
+                    animation.animatedFraction,
+                    animation.animatedValue as Float,
+                    UdfpsKeyguardView.ANIMATION_UNLOCKED_SCREEN_OFF
+                )
+            }
+        }
+    /**
+     * Hidden amount of input (pin/pattern/password) bouncer. This is used
+     * [KeyguardBouncer.EXPANSION_VISIBLE] (0f) to [KeyguardBouncer.EXPANSION_HIDDEN] (1f). Only
+     * used for the non-modernBouncer.
+     */
+    private var inputBouncerHiddenAmount = KeyguardBouncer.EXPANSION_HIDDEN
+    private var inputBouncerExpansion = 0f // only used for modernBouncer
+
+    private val stateListener: StatusBarStateController.StateListener =
+        object : StatusBarStateController.StateListener {
+            override fun onDozeAmountChanged(linear: Float, eased: Float) {
+                if (lastDozeAmount < linear) {
+                    showUdfpsBouncer(false)
+                }
+                unlockedScreenOffDozeAnimator.cancel()
+                val animatingFromUnlockedScreenOff =
+                    unlockedScreenOffAnimationController.isAnimationPlaying()
+                if (animatingFromUnlockedScreenOff && linear != 0f) {
+                    // we manually animate the fade in of the UDFPS icon since the unlocked
+                    // screen off animation prevents the doze amounts to be incrementally eased in
+                    unlockedScreenOffDozeAnimator.start()
+                } else {
+                    view.onDozeAmountChanged(
+                        linear,
+                        eased,
+                        UdfpsKeyguardView.ANIMATION_BETWEEN_AOD_AND_LOCKSCREEN
+                    )
+                }
+                lastDozeAmount = linear
+                updatePauseAuth()
+            }
+
+            override fun onStateChanged(statusBarState: Int) {
+                this@UdfpsKeyguardViewController.statusBarState = statusBarState
+                updateAlpha()
+                updatePauseAuth()
+            }
+        }
+
+    private val bouncerExpansionCallback: BouncerExpansionCallback =
+        object : BouncerExpansionCallback {
+            override fun onExpansionChanged(expansion: Float) {
+                inputBouncerHiddenAmount = expansion
+                updateAlpha()
+                updatePauseAuth()
+            }
+
+            override fun onVisibilityChanged(isVisible: Boolean) {
+                updateBouncerHiddenAmount()
+                updateAlpha()
+                updatePauseAuth()
+            }
+        }
+
+    private val configurationListener: ConfigurationController.ConfigurationListener =
+        object : ConfigurationController.ConfigurationListener {
+            override fun onUiModeChanged() {
+                view.updateColor()
+            }
+
+            override fun onThemeChanged() {
+                view.updateColor()
+            }
+
+            override fun onConfigChanged(newConfig: Configuration) {
+                updateScaleFactor()
+                view.updatePadding()
+                view.updateColor()
+            }
+        }
+
+    private val shadeExpansionListener = ShadeExpansionListener { (fraction) ->
+        panelExpansionFraction =
+            if (keyguardViewManager.isBouncerInTransit) {
+                aboutToShowBouncerProgress(fraction)
+            } else {
+                fraction
+            }
+        updateAlpha()
+        updatePauseAuth()
+    }
+
+    private val keyguardStateControllerCallback: KeyguardStateController.Callback =
+        object : KeyguardStateController.Callback {
+            override fun onLaunchTransitionFadingAwayChanged() {
+                launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway
+                updatePauseAuth()
+            }
+        }
+
+    private val activityLaunchAnimatorListener: ActivityLaunchAnimator.Listener =
+        object : ActivityLaunchAnimator.Listener {
+            override fun onLaunchAnimationStart() {
+                isLaunchingActivity = true
+                activityLaunchProgress = 0f
+                updateAlpha()
+            }
+
+            override fun onLaunchAnimationEnd() {
+                isLaunchingActivity = false
+                updateAlpha()
+            }
+
+            override fun onLaunchAnimationProgress(linearProgress: Float) {
+                activityLaunchProgress = linearProgress
+                updateAlpha()
+            }
+        }
+
+    private val statusBarKeyguardViewManagerCallback: KeyguardViewManagerCallback =
+        object : KeyguardViewManagerCallback {
+            override fun onQSExpansionChanged(qsExpansion: Float) {
+                this@UdfpsKeyguardViewController.qsExpansion = qsExpansion
+                updateAlpha()
+                updatePauseAuth()
+            }
+
+            /**
+             * Forward touches to the UdfpsController. This allows the touch to start from outside
+             * the sensor area and then slide their finger into the sensor area.
+             */
+            override fun onTouch(event: MotionEvent) {
+                // Don't forward touches if the shade has already started expanding.
+                if (transitionToFullShadeProgress != 0f) {
+                    return
+                }
+                udfpsController.onTouch(event)
+            }
+        }
+
+    private val alternateAuthInterceptor: AlternateAuthInterceptor =
+        object : AlternateAuthInterceptor {
+            override fun showAlternateAuthBouncer(): Boolean {
+                return showUdfpsBouncer(true)
+            }
+
+            override fun hideAlternateAuthBouncer(): Boolean {
+                return showUdfpsBouncer(false)
+            }
+
+            override fun isShowingAlternateAuthBouncer(): Boolean {
+                return showingUdfpsBouncer
+            }
+
+            override fun requestUdfps(request: Boolean, color: Int) {
+                udfpsRequested = request
+                view.requestUdfps(request, color)
+                updateAlpha()
+                updatePauseAuth()
+            }
+
+            override fun dump(pw: PrintWriter) {
+                pw.println(tag)
+            }
+        }
+
+    override val tag: String
+        get() = TAG
+
+    override fun onInit() {
+        super.onInit()
+        keyguardViewManager.setAlternateAuthInterceptor(alternateAuthInterceptor)
+    }
+
+    init {
+        if (isModernBouncerEnabled) {
+            view.repeatWhenAttached {
+                // repeatOnLifecycle CREATED (as opposed to STARTED) because the Bouncer expansion
+                // can make the view not visible; and we still want to listen for events
+                // that may make the view visible again.
+                repeatOnLifecycle(Lifecycle.State.CREATED) { listenForBouncerExpansion(this) }
+            }
+        }
+    }
+
+    @VisibleForTesting
+    internal suspend fun listenForBouncerExpansion(scope: CoroutineScope): Job {
+        return scope.launch {
+            bouncerInteractor.bouncerExpansion.collect { bouncerExpansion: Float ->
+                inputBouncerExpansion = bouncerExpansion
+                updateAlpha()
+                updatePauseAuth()
+            }
+        }
+    }
+
+    public override fun onViewAttached() {
+        super.onViewAttached()
+        val dozeAmount = statusBarStateController.dozeAmount
+        lastDozeAmount = dozeAmount
+        stateListener.onDozeAmountChanged(dozeAmount, dozeAmount)
+        statusBarStateController.addCallback(stateListener)
+        udfpsRequested = false
+        launchTransitionFadingAway = keyguardStateController.isLaunchTransitionFadingAway
+        keyguardStateController.addCallback(keyguardStateControllerCallback)
+        statusBarState = statusBarStateController.state
+        qsExpansion = keyguardViewManager.qsExpansion
+        keyguardViewManager.addCallback(statusBarKeyguardViewManagerCallback)
+        if (!isModernBouncerEnabled) {
+            val bouncer = keyguardViewManager.bouncer
+            bouncer?.expansion?.let {
+                bouncerExpansionCallback.onExpansionChanged(it)
+                bouncer.addBouncerExpansionCallback(bouncerExpansionCallback)
+            }
+            updateBouncerHiddenAmount()
+        }
+        configurationController.addCallback(configurationListener)
+        shadeExpansionStateManager.addExpansionListener(shadeExpansionListener)
+        updateScaleFactor()
+        view.updatePadding()
+        updateAlpha()
+        updatePauseAuth()
+        keyguardViewManager.setAlternateAuthInterceptor(alternateAuthInterceptor)
+        lockScreenShadeTransitionController.udfpsKeyguardViewController = this
+        activityLaunchAnimator.addListener(activityLaunchAnimatorListener)
+    }
+
+    override fun onViewDetached() {
+        super.onViewDetached()
+        faceDetectRunning = false
+        keyguardStateController.removeCallback(keyguardStateControllerCallback)
+        statusBarStateController.removeCallback(stateListener)
+        keyguardViewManager.removeAlternateAuthInterceptor(alternateAuthInterceptor)
+        keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
+        configurationController.removeCallback(configurationListener)
+        shadeExpansionStateManager.removeExpansionListener(shadeExpansionListener)
+        if (lockScreenShadeTransitionController.udfpsKeyguardViewController === this) {
+            lockScreenShadeTransitionController.udfpsKeyguardViewController = null
+        }
+        activityLaunchAnimator.removeListener(activityLaunchAnimatorListener)
+        keyguardViewManager.removeCallback(statusBarKeyguardViewManagerCallback)
+        if (!isModernBouncerEnabled) {
+            keyguardViewManager.bouncer?.removeBouncerExpansionCallback(bouncerExpansionCallback)
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<String>) {
+        super.dump(pw, args)
+        pw.println("isModernBouncerEnabled=$isModernBouncerEnabled")
+        pw.println("showingUdfpsAltBouncer=$showingUdfpsBouncer")
+        pw.println("faceDetectRunning=$faceDetectRunning")
+        pw.println("statusBarState=" + StatusBarState.toString(statusBarState))
+        pw.println("transitionToFullShadeProgress=$transitionToFullShadeProgress")
+        pw.println("qsExpansion=$qsExpansion")
+        pw.println("panelExpansionFraction=$panelExpansionFraction")
+        pw.println("unpausedAlpha=" + view.unpausedAlpha)
+        pw.println("udfpsRequestedByApp=$udfpsRequested")
+        pw.println("launchTransitionFadingAway=$launchTransitionFadingAway")
+        pw.println("lastDozeAmount=$lastDozeAmount")
+        if (isModernBouncerEnabled) {
+            pw.println("inputBouncerExpansion=$inputBouncerExpansion")
+        } else {
+            pw.println("inputBouncerHiddenAmount=$inputBouncerHiddenAmount")
+        }
+        view.dump(pw)
+    }
+
+    /**
+     * Overrides non-bouncer show logic in shouldPauseAuth to still show icon.
+     * @return whether the udfpsBouncer has been newly shown or hidden
+     */
+    private fun showUdfpsBouncer(show: Boolean): Boolean {
+        if (showingUdfpsBouncer == show) {
+            return false
+        }
+        val udfpsAffordanceWasNotShowing = shouldPauseAuth()
+        showingUdfpsBouncer = show
+        if (showingUdfpsBouncer) {
+            lastUdfpsBouncerShowTime = systemClock.uptimeMillis()
+        }
+        if (showingUdfpsBouncer) {
+            if (udfpsAffordanceWasNotShowing) {
+                view.animateInUdfpsBouncer(null)
+            }
+            if (keyguardStateController.isOccluded) {
+                keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(true)
+            }
+            view.announceForAccessibility(
+                view.context.getString(R.string.accessibility_fingerprint_bouncer)
+            )
+        } else {
+            keyguardUpdateMonitor.requestFaceAuthOnOccludingApp(false)
+        }
+        updateBouncerHiddenAmount()
+        updateAlpha()
+        updatePauseAuth()
+        return true
+    }
+
+    /**
+     * Returns true if the fingerprint manager is running but we want to temporarily pause
+     * authentication. On the keyguard, we may want to show udfps when the shade is expanded, so
+     * this can be overridden with the showBouncer method.
+     */
+    override fun shouldPauseAuth(): Boolean {
+        if (showingUdfpsBouncer) {
+            return false
+        }
+        if (
+            udfpsRequested &&
+                !notificationShadeVisible &&
+                !isInputBouncerFullyVisible() &&
+                keyguardStateController.isShowing
+        ) {
+            return false
+        }
+        if (launchTransitionFadingAway) {
+            return true
+        }
+
+        // Only pause auth if we're not on the keyguard AND we're not transitioning to doze
+        // (ie: dozeAmount = 0f). For the UnlockedScreenOffAnimation, the statusBarState is
+        // delayed. However, we still animate in the UDFPS affordance with the
+        // mUnlockedScreenOffDozeAnimator.
+        if (statusBarState != StatusBarState.KEYGUARD && lastDozeAmount == 0f) {
+            return true
+        }
+        if (isBouncerExpansionGreaterThan(.5f)) {
+            return true
+        }
+        return view.unpausedAlpha < 255 * .1
+    }
+
+    fun isBouncerExpansionGreaterThan(bouncerExpansionThreshold: Float): Boolean {
+        return if (isModernBouncerEnabled) {
+            inputBouncerExpansion >= bouncerExpansionThreshold
+        } else {
+            inputBouncerHiddenAmount < bouncerExpansionThreshold
+        }
+    }
+
+    fun isInputBouncerFullyVisible(): Boolean {
+        return if (isModernBouncerEnabled) {
+            inputBouncerExpansion == 1f
+        } else {
+            keyguardViewManager.isBouncerShowing && !keyguardViewManager.isShowingAlternateAuth
+        }
+    }
+
+    override fun listenForTouchesOutsideView(): Boolean {
+        return true
+    }
+
+    override fun onTouchOutsideView() {
+        maybeShowInputBouncer()
+    }
+
+    /**
+     * If we were previously showing the udfps bouncer, hide it and instead show the regular
+     * (pin/pattern/password) bouncer.
+     *
+     * Does nothing if we weren't previously showing the UDFPS bouncer.
+     */
+    private fun maybeShowInputBouncer() {
+        if (showingUdfpsBouncer && hasUdfpsBouncerShownWithMinTime()) {
+            keyguardViewManager.showBouncer(true)
+        }
+    }
+
+    /**
+     * Whether the udfps bouncer has shown for at least 200ms before allowing touches outside of the
+     * udfps icon area to dismiss the udfps bouncer and show the pin/pattern/password bouncer.
+     */
+    private fun hasUdfpsBouncerShownWithMinTime(): Boolean {
+        return systemClock.uptimeMillis() - lastUdfpsBouncerShowTime > 200
+    }
+
+    /**
+     * Set the progress we're currently transitioning to the full shade. 0.0f means we're not
+     * transitioning yet, while 1.0f means we've fully dragged down. For example, start swiping down
+     * to expand the notification shade from the empty space in the middle of the lock screen.
+     */
+    fun setTransitionToFullShadeProgress(progress: Float) {
+        transitionToFullShadeProgress = progress
+        updateAlpha()
+    }
+
+    /**
+     * Update alpha for the UDFPS lock screen affordance. The AoD UDFPS visual affordance's alpha is
+     * based on the doze amount.
+     */
+    override fun updateAlpha() {
+        // Fade icon on transitions to showing the status bar or bouncer, but if mUdfpsRequested,
+        // then the keyguard is occluded by some application - so instead use the input bouncer
+        // hidden amount to determine the fade.
+        val expansion = if (udfpsRequested) getInputBouncerHiddenAmt() else panelExpansionFraction
+        var alpha: Int =
+            if (showingUdfpsBouncer) 255
+            else MathUtils.constrain(MathUtils.map(.5f, .9f, 0f, 255f, expansion), 0f, 255f).toInt()
+        if (!showingUdfpsBouncer) {
+            // swipe from top of the lockscreen to expand full QS:
+            alpha =
+                (alpha * (1.0f - Interpolators.EMPHASIZED_DECELERATE.getInterpolation(qsExpansion)))
+                    .toInt()
+
+            // swipe from the middle (empty space) of lockscreen to expand the notification shade:
+            alpha = (alpha * (1.0f - transitionToFullShadeProgress)).toInt()
+
+            // Fade out the icon if we are animating an activity launch over the lockscreen and the
+            // activity didn't request the UDFPS.
+            if (isLaunchingActivity && !udfpsRequested) {
+                alpha = (alpha * (1.0f - activityLaunchProgress)).toInt()
+            }
+
+            // Fade out alpha when a dialog is shown
+            // Fade in alpha when a dialog is hidden
+            alpha = (alpha * view.dialogSuggestedAlpha).toInt()
+        }
+        view.unpausedAlpha = alpha
+    }
+
+    private fun getInputBouncerHiddenAmt(): Float {
+        return if (isModernBouncerEnabled) {
+            1f - inputBouncerExpansion
+        } else {
+            inputBouncerHiddenAmount
+        }
+    }
+
+    /** Update the scale factor based on the device's resolution. */
+    private fun updateScaleFactor() {
+        udfpsController.mOverlayParams?.scaleFactor?.let { view.setScaleFactor(it) }
+    }
+
+    private fun updateBouncerHiddenAmount() {
+        if (isModernBouncerEnabled) {
+            return
+        }
+        val altBouncerShowing = keyguardViewManager.isShowingAlternateAuth
+        if (altBouncerShowing || !keyguardViewManager.bouncerIsOrWillBeShowing()) {
+            inputBouncerHiddenAmount = 1f
+        } else if (keyguardViewManager.isBouncerShowing) {
+            // input bouncer is fully showing
+            inputBouncerHiddenAmount = 0f
+        }
+    }
+
+    companion object {
+        const val TAG = "UdfpsKeyguardViewController"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
new file mode 100644
index 0000000..d8dd6a2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.dreams
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.AnimatorSet
+import android.animation.ValueAnimator
+import android.view.View
+import androidx.core.animation.doOnEnd
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dreams.complication.ComplicationHostViewController
+import com.android.systemui.dreams.complication.ComplicationLayoutParams
+import com.android.systemui.dreams.dagger.DreamOverlayModule
+import com.android.systemui.statusbar.BlurUtils
+import java.util.function.Consumer
+import javax.inject.Inject
+import javax.inject.Named
+
+/** Controller for dream overlay animations. */
+class DreamOverlayAnimationsController
+@Inject
+constructor(
+    private val mBlurUtils: BlurUtils,
+    private val mComplicationHostViewController: ComplicationHostViewController,
+    private val mStatusBarViewController: DreamOverlayStatusBarViewController,
+    private val mOverlayStateController: DreamOverlayStateController,
+    @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DURATION)
+    private val mDreamInBlurAnimDuration: Int,
+    @Named(DreamOverlayModule.DREAM_IN_BLUR_ANIMATION_DELAY) private val mDreamInBlurAnimDelay: Int,
+    @Named(DreamOverlayModule.DREAM_IN_COMPLICATIONS_ANIMATION_DURATION)
+    private val mDreamInComplicationsAnimDuration: Int,
+    @Named(DreamOverlayModule.DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY)
+    private val mDreamInTopComplicationsAnimDelay: Int,
+    @Named(DreamOverlayModule.DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY)
+    private val mDreamInBottomComplicationsAnimDelay: Int
+) {
+
+    var mEntryAnimations: AnimatorSet? = null
+
+    /** Starts the dream content and dream overlay entry animations. */
+    fun startEntryAnimations(view: View) {
+        cancelRunningEntryAnimations()
+
+        mEntryAnimations = AnimatorSet()
+        mEntryAnimations?.apply {
+            playTogether(
+                buildDreamInBlurAnimator(view),
+                buildDreamInTopComplicationsAnimator(),
+                buildDreamInBottomComplicationsAnimator()
+            )
+            doOnEnd { mOverlayStateController.setEntryAnimationsFinished(true) }
+            start()
+        }
+    }
+
+    /** Cancels the dream content and dream overlay animations, if they're currently running. */
+    fun cancelRunningEntryAnimations() {
+        if (mEntryAnimations?.isRunning == true) {
+            mEntryAnimations?.cancel()
+        }
+        mEntryAnimations = null
+    }
+
+    private fun buildDreamInBlurAnimator(view: View): Animator {
+        return ValueAnimator.ofFloat(1f, 0f).apply {
+            duration = mDreamInBlurAnimDuration.toLong()
+            startDelay = mDreamInBlurAnimDelay.toLong()
+            interpolator = Interpolators.LINEAR
+            addUpdateListener { animator: ValueAnimator ->
+                mBlurUtils.applyBlur(
+                    view.viewRootImpl,
+                    mBlurUtils.blurRadiusOfRatio(animator.animatedValue as Float).toInt(),
+                    false /*opaque*/
+                )
+            }
+        }
+    }
+
+    private fun buildDreamInTopComplicationsAnimator(): Animator {
+        return ValueAnimator.ofFloat(0f, 1f).apply {
+            duration = mDreamInComplicationsAnimDuration.toLong()
+            startDelay = mDreamInTopComplicationsAnimDelay.toLong()
+            interpolator = Interpolators.LINEAR
+            addUpdateListener { va: ValueAnimator ->
+                setTopElementsAlpha(va.animatedValue as Float)
+            }
+        }
+    }
+
+    private fun buildDreamInBottomComplicationsAnimator(): Animator {
+        return ValueAnimator.ofFloat(0f, 1f).apply {
+            duration = mDreamInComplicationsAnimDuration.toLong()
+            startDelay = mDreamInBottomComplicationsAnimDelay.toLong()
+            interpolator = Interpolators.LINEAR
+            addUpdateListener { va: ValueAnimator ->
+                setBottomElementsAlpha(va.animatedValue as Float)
+            }
+            addListener(
+                object : AnimatorListenerAdapter() {
+                    override fun onAnimationStart(animation: Animator) {
+                        mComplicationHostViewController
+                            .getViewsAtPosition(ComplicationLayoutParams.POSITION_BOTTOM)
+                            .forEach(Consumer { v: View -> v.visibility = View.VISIBLE })
+                    }
+                }
+            )
+        }
+    }
+
+    /** Sets alpha of top complications and the status bar. */
+    private fun setTopElementsAlpha(alpha: Float) {
+        mComplicationHostViewController
+            .getViewsAtPosition(ComplicationLayoutParams.POSITION_TOP)
+            .forEach(Consumer { v: View -> setAlphaAndEnsureVisible(v, alpha) })
+        mStatusBarViewController.setAlpha(alpha)
+    }
+
+    /** Sets alpha of bottom complications. */
+    private fun setBottomElementsAlpha(alpha: Float) {
+        mComplicationHostViewController
+            .getViewsAtPosition(ComplicationLayoutParams.POSITION_BOTTOM)
+            .forEach(Consumer { v: View -> setAlphaAndEnsureVisible(v, alpha) })
+    }
+
+    private fun setAlphaAndEnsureVisible(view: View, alpha: Float) {
+        if (alpha > 0 && view.visibility != View.VISIBLE) {
+            view.visibility = View.VISIBLE
+        }
+
+        view.alpha = alpha
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 733a80d..d0d0184 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -54,6 +54,8 @@
     private final DreamOverlayStatusBarViewController mStatusBarViewController;
     private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     private final BlurUtils mBlurUtils;
+    private final DreamOverlayAnimationsController mDreamOverlayAnimationsController;
+    private final DreamOverlayStateController mStateController;
 
     private final ComplicationHostViewController mComplicationHostViewController;
 
@@ -134,12 +136,16 @@
             @Named(DreamOverlayModule.BURN_IN_PROTECTION_UPDATE_INTERVAL) long
                     burnInProtectionUpdateInterval,
             @Named(DreamOverlayModule.MILLIS_UNTIL_FULL_JITTER) long millisUntilFullJitter,
-            BouncerCallbackInteractor bouncerCallbackInteractor) {
+            BouncerCallbackInteractor bouncerCallbackInteractor,
+            DreamOverlayAnimationsController animationsController,
+            DreamOverlayStateController stateController) {
         super(containerView);
         mDreamOverlayContentView = contentView;
         mStatusBarViewController = statusBarViewController;
         mStatusBarKeyguardViewManager = statusBarKeyguardViewManager;
         mBlurUtils = blurUtils;
+        mDreamOverlayAnimationsController = animationsController;
+        mStateController = stateController;
 
         mComplicationHostViewController = complicationHostViewController;
         mDreamOverlayMaxTranslationY = resources.getDimensionPixelSize(
@@ -172,6 +178,11 @@
             bouncer.addBouncerExpansionCallback(mBouncerExpansionCallback);
         }
         mBouncerCallbackInteractor.addBouncerExpansionCallback(mBouncerExpansionCallback);
+
+        // Start dream entry animations. Skip animations for low light clock.
+        if (!mStateController.isLowLightActive()) {
+            mDreamOverlayAnimationsController.startEntryAnimations(mView);
+        }
     }
 
     @Override
@@ -182,6 +193,8 @@
             bouncer.removeBouncerExpansionCallback(mBouncerExpansionCallback);
         }
         mBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
+
+        mDreamOverlayAnimationsController.cancelRunningEntryAnimations();
     }
 
     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 d1b7368..6380fd5 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -257,6 +257,7 @@
 
         mStateController.setOverlayActive(false);
         mStateController.setLowLightActive(false);
+        mStateController.setEntryAnimationsFinished(false);
 
         mDreamOverlayContainerViewController = null;
         mDreamOverlayTouchMonitor = null;
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
index 72feaca..e80d0be 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStateController.java
@@ -51,6 +51,7 @@
 
     public static final int STATE_DREAM_OVERLAY_ACTIVE = 1 << 0;
     public static final int STATE_LOW_LIGHT_ACTIVE = 1 << 1;
+    public static final int STATE_DREAM_ENTRY_ANIMATIONS_FINISHED = 1 << 2;
 
     private static final int OP_CLEAR_STATE = 1;
     private static final int OP_SET_STATE = 2;
@@ -202,6 +203,14 @@
         return containsState(STATE_LOW_LIGHT_ACTIVE);
     }
 
+    /**
+     * Returns whether the dream content and dream overlay entry animations are finished.
+     * @return {@code true} if animations are finished, {@code false} otherwise.
+     */
+    public boolean areEntryAnimationsFinished() {
+        return containsState(STATE_DREAM_ENTRY_ANIMATIONS_FINISHED);
+    }
+
     private boolean containsState(int state) {
         return (mState & state) != 0;
     }
@@ -218,7 +227,7 @@
         }
 
         if (existingState != mState) {
-            notifyCallbacks(callback -> callback.onStateChanged());
+            notifyCallbacks(Callback::onStateChanged);
         }
     }
 
@@ -239,6 +248,15 @@
     }
 
     /**
+     * Sets whether dream content and dream overlay entry animations are finished.
+     * @param finished {@code true} if entry animations are finished, {@code false} otherwise.
+     */
+    public void setEntryAnimationsFinished(boolean finished) {
+        modifyState(finished ? OP_SET_STATE : OP_CLEAR_STATE,
+                STATE_DREAM_ENTRY_ANIMATIONS_FINISHED);
+    }
+
+    /**
      * 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 bb1c430..d17fbe3 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayStatusBarViewController.java
@@ -16,10 +16,6 @@
 
 package com.android.systemui.dreams;
 
-import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
-import static android.app.StatusBarManager.WINDOW_STATE_HIDING;
-import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
-
 import android.app.AlarmManager;
 import android.app.StatusBarManager;
 import android.content.res.Resources;
@@ -83,6 +79,9 @@
 
     private boolean mIsAttached;
 
+    // Whether dream entry animations are finished.
+    private boolean mEntryAnimationsFinished = false;
+
     private final NetworkRequest mNetworkRequest = new NetworkRequest.Builder()
             .clearCapabilities()
             .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
@@ -109,7 +108,9 @@
             new DreamOverlayStateController.Callback() {
                 @Override
                 public void onStateChanged() {
-                    updateLowLightState();
+                    mEntryAnimationsFinished =
+                            mDreamOverlayStateController.areEntryAnimationsFinished();
+                    updateVisibility();
                 }
             };
 
@@ -195,7 +196,6 @@
         mStatusBarItemsProvider.addCallback(mStatusBarItemsProviderCallback);
 
         mDreamOverlayStateController.addCallback(mDreamOverlayStateCallback);
-        updateLowLightState();
 
         mTouchInsetSession.addViewToTracking(mView);
     }
@@ -216,6 +216,26 @@
         mIsAttached = false;
     }
 
+    /**
+     * Sets alpha of the dream overlay status bar.
+     *
+     * No-op if the dream overlay status bar should not be shown.
+     */
+    protected void setAlpha(float alpha) {
+        updateVisibility();
+
+        if (mView.getVisibility() != View.VISIBLE) {
+            return;
+        }
+
+        mView.setAlpha(alpha);
+    }
+
+    private boolean shouldShowStatusBar() {
+        return !mDreamOverlayStateController.isLowLightActive()
+                && !mStatusBarWindowStateController.windowIsShowing();
+    }
+
     private void updateWifiUnavailableStatusIcon() {
         final NetworkCapabilities capabilities =
                 mConnectivityManager.getNetworkCapabilities(
@@ -235,13 +255,12 @@
                 hasAlarm ? buildAlarmContentDescription(alarm) : null);
     }
 
-    private void updateLowLightState() {
-        int visibility = View.VISIBLE;
-        if (mDreamOverlayStateController.isLowLightActive()
-                || mStatusBarWindowStateController.windowIsShowing()) {
-            visibility = View.INVISIBLE;
+    private void updateVisibility() {
+        if (shouldShowStatusBar()) {
+            mView.setVisibility(View.VISIBLE);
+        } else {
+            mView.setVisibility(View.INVISIBLE);
         }
-        mView.setVisibility(visibility);
     }
 
     private String buildAlarmContentDescription(AlarmManager.AlarmClockInfo alarm) {
@@ -298,21 +317,11 @@
     }
 
     private void onSystemStatusBarStateChanged(@StatusBarManager.WindowVisibleState int state) {
-        mMainExecutor.execute(() -> {
-            if (!mIsAttached || mDreamOverlayStateController.isLowLightActive()) {
-                return;
-            }
+        if (!mIsAttached || !mEntryAnimationsFinished) {
+            return;
+        }
 
-            switch (state) {
-                case WINDOW_STATE_SHOWING:
-                    mView.setVisibility(View.INVISIBLE);
-                    break;
-                case WINDOW_STATE_HIDING:
-                case WINDOW_STATE_HIDDEN:
-                    mView.setVisibility(View.VISIBLE);
-                    break;
-            }
-        });
+        mMainExecutor.execute(this::updateVisibility);
     }
 
     private void onStatusBarItemsChanged(List<StatusBarItem> newItems) {
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
index fd6cfc0..100ccc3 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/complication/ComplicationHostViewController.java
@@ -28,6 +28,7 @@
 import androidx.constraintlayout.widget.ConstraintLayout;
 import androidx.lifecycle.LifecycleOwner;
 
+import com.android.systemui.dreams.DreamOverlayStateController;
 import com.android.systemui.util.ViewController;
 
 import java.util.Collection;
@@ -49,20 +50,34 @@
     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
 
     private final ComplicationLayoutEngine mLayoutEngine;
+    private final DreamOverlayStateController mDreamOverlayStateController;
     private final LifecycleOwner mLifecycleOwner;
     private final ComplicationCollectionViewModel mComplicationCollectionViewModel;
     private final HashMap<ComplicationId, Complication.ViewHolder> mComplications = new HashMap<>();
 
+    // Whether dream entry animations are finished.
+    private boolean mEntryAnimationsFinished = false;
+
     @Inject
     protected ComplicationHostViewController(
             @Named(SCOPED_COMPLICATIONS_LAYOUT) ConstraintLayout view,
             ComplicationLayoutEngine layoutEngine,
+            DreamOverlayStateController dreamOverlayStateController,
             LifecycleOwner lifecycleOwner,
             @Named(SCOPED_COMPLICATIONS_MODEL) ComplicationCollectionViewModel viewModel) {
         super(view);
         mLayoutEngine = layoutEngine;
         mLifecycleOwner = lifecycleOwner;
         mComplicationCollectionViewModel = viewModel;
+        mDreamOverlayStateController = dreamOverlayStateController;
+
+        mDreamOverlayStateController.addCallback(new DreamOverlayStateController.Callback() {
+            @Override
+            public void onStateChanged() {
+                mEntryAnimationsFinished =
+                        mDreamOverlayStateController.areEntryAnimationsFinished();
+            }
+        });
     }
 
     @Override
@@ -123,6 +138,11 @@
                     final ComplicationId id = complication.getId();
                     final Complication.ViewHolder viewHolder = complication.getComplication()
                             .createView(complication);
+                    // Complications to be added before dream entry animations are finished are set
+                    // to invisible and are animated in.
+                    if (!mEntryAnimationsFinished) {
+                        viewHolder.getView().setVisibility(View.INVISIBLE);
+                    }
                     mComplications.put(id, viewHolder);
                     if (viewHolder.getView().getParent() != null) {
                         Log.e(TAG, "View for complication "
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
index 4fe1622..cb012fa 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamOverlayModule.java
@@ -47,6 +47,14 @@
     public static final String BURN_IN_PROTECTION_UPDATE_INTERVAL =
             "burn_in_protection_update_interval";
     public static final String MILLIS_UNTIL_FULL_JITTER = "millis_until_full_jitter";
+    public static final String DREAM_IN_BLUR_ANIMATION_DURATION = "dream_in_blur_anim_duration";
+    public static final String DREAM_IN_BLUR_ANIMATION_DELAY = "dream_in_blur_anim_delay";
+    public static final String DREAM_IN_COMPLICATIONS_ANIMATION_DURATION =
+            "dream_in_complications_anim_duration";
+    public static final String DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY =
+            "dream_in_top_complications_anim_delay";
+    public static final String DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY =
+            "dream_in_bottom_complications_anim_delay";
 
     /** */
     @Provides
@@ -114,6 +122,51 @@
         return resources.getInteger(R.integer.config_dreamOverlayMillisUntilFullJitter);
     }
 
+    /**
+     * Duration in milliseconds of the dream in un-blur animation.
+     */
+    @Provides
+    @Named(DREAM_IN_BLUR_ANIMATION_DURATION)
+    static int providesDreamInBlurAnimationDuration(@Main Resources resources) {
+        return resources.getInteger(R.integer.config_dreamOverlayInBlurDurationMs);
+    }
+
+    /**
+     * Delay in milliseconds of the dream in un-blur animation.
+     */
+    @Provides
+    @Named(DREAM_IN_BLUR_ANIMATION_DELAY)
+    static int providesDreamInBlurAnimationDelay(@Main Resources resources) {
+        return resources.getInteger(R.integer.config_dreamOverlayInBlurDelayMs);
+    }
+
+    /**
+     * Duration in milliseconds of the dream in complications fade-in animation.
+     */
+    @Provides
+    @Named(DREAM_IN_COMPLICATIONS_ANIMATION_DURATION)
+    static int providesDreamInComplicationsAnimationDuration(@Main Resources resources) {
+        return resources.getInteger(R.integer.config_dreamOverlayInComplicationsDurationMs);
+    }
+
+    /**
+     * Delay in milliseconds of the dream in top complications fade-in animation.
+     */
+    @Provides
+    @Named(DREAM_IN_TOP_COMPLICATIONS_ANIMATION_DELAY)
+    static int providesDreamInTopComplicationsAnimationDelay(@Main Resources resources) {
+        return resources.getInteger(R.integer.config_dreamOverlayInTopComplicationsDelayMs);
+    }
+
+    /**
+     * Delay in milliseconds of the dream in bottom complications fade-in animation.
+     */
+    @Provides
+    @Named(DREAM_IN_BOTTOM_COMPLICATIONS_ANIMATION_DELAY)
+    static int providesDreamInBottomComplicationsAnimationDelay(@Main Resources resources) {
+        return resources.getInteger(R.integer.config_dreamOverlayInBottomComplicationsDelayMs);
+    }
+
     @Provides
     @DreamOverlayComponent.DreamOverlayScope
     static LifecycleOwner providesLifecycleOwner(Lazy<LifecycleRegistry> lifecycleRegistryLazy) {
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index 3f7a5ecb..09d7c2b 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -125,6 +125,14 @@
     // TODO(b/255607168): Tracking Bug
     @JvmField val DOZING_MIGRATION_1 = UnreleasedFlag(213)
 
+    /**
+     * Whether to enable the code powering customizable lock screen quick affordances.
+     *
+     * Note that this flag does not enable individual implementations of quick affordances like the
+     * new camera quick affordance. Look for individual flags for those.
+     */
+    @JvmField val CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES = UnreleasedFlag(214, teamfood = false)
+
     // 300 - power menu
     // TODO(b/254512600): Tracking Bug
     @JvmField val POWER_MENU_LITE = ReleasedFlag(300)
@@ -171,10 +179,18 @@
     @Deprecated("Replaced by mobile and wifi specific flags.")
     val NEW_STATUS_BAR_PIPELINE_FRONTEND = UnreleasedFlag(605, teamfood = false)
 
+    // TODO(b/256614753): Tracking Bug
     val NEW_STATUS_BAR_MOBILE_ICONS = UnreleasedFlag(606)
 
+    // TODO(b/256614210): Tracking Bug
     val NEW_STATUS_BAR_WIFI_ICON = UnreleasedFlag(607)
 
+    // TODO(b/256614751): Tracking Bug
+    val NEW_STATUS_BAR_MOBILE_ICONS_BACKEND = UnreleasedFlag(608)
+
+    // TODO(b/256613548): Tracking Bug
+    val NEW_STATUS_BAR_WIFI_ICON_BACKEND = UnreleasedFlag(609)
+
     // 700 - dialer/calls
     // TODO(b/254512734): Tracking Bug
     val ONGOING_CALL_STATUS_BAR_CHIP = ReleasedFlag(700)
@@ -327,7 +343,7 @@
     val CHOOSER_UNBUNDLED = UnreleasedFlag(1500, teamfood = true)
 
     // 1700 - clipboard
-    @JvmField val CLIPBOARD_OVERLAY_REFACTOR = UnreleasedFlag(1700)
+    @JvmField val CLIPBOARD_OVERLAY_REFACTOR = UnreleasedFlag(1700, true)
     @JvmField val CLIPBOARD_REMOTE_BEHAVIOR = UnreleasedFlag(1701)
 
     // 1800 - shade container
@@ -336,6 +352,9 @@
     // 1900 - note task
     @JvmField val NOTE_TASKS = SysPropBooleanFlag(1900, "persist.sysui.debug.note_tasks")
 
+    // 2000 - device controls
+    @Keep val USE_APP_PANELS = UnreleasedFlag(2000, true)
+
     // Pay no attention to the reflection behind the curtain.
     // ========================== Curtain ==========================
     // |                                                           |
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
index 56a1f1a..f08463b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java
@@ -42,6 +42,7 @@
 import com.android.systemui.keyguard.DismissCallbackRegistry;
 import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
 import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule;
 import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule;
 import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule;
 import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceModule;
@@ -71,6 +72,7 @@
         KeyguardUserSwitcherComponent.class},
         includes = {
             FalsingModule.class,
+            KeyguardDataQuickAffordanceModule.class,
             KeyguardQuickAffordanceModule.class,
             KeyguardRepositoryModule.class,
             StartKeyguardTransitionModule.class,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
index 99ae85d..80c6130 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/BouncerView.kt
@@ -18,6 +18,7 @@
 
 import android.view.KeyEvent
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.plugins.ActivityStarter
 import java.lang.ref.WeakReference
 import javax.inject.Inject
 
@@ -45,4 +46,9 @@
     fun dispatchBackKeyEventPreIme(): Boolean
     fun showNextSecurityScreenOrFinish(): Boolean
     fun resume()
+    fun setDismissAction(
+        onDismissAction: ActivityStarter.OnDismissAction?,
+        cancelAction: Runnable?,
+    )
+    fun willDismissWithActions(): Boolean
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
index c600e13..d6f521c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfig.kt
@@ -53,6 +53,10 @@
 
     override val key: String = BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
 
+    override val pickerName: String by lazy { context.getString(component.getTileTitleId()) }
+
+    override val pickerIconResourceId: Int by lazy { component.getTileImageId() }
+
     override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
         component.canShowWhileLockedSetting.flatMapLatest { canShowWhileLocked ->
             if (canShowWhileLocked) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
new file mode 100644
index 0000000..bea9363
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardDataQuickAffordanceModule.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import dagger.Module
+import dagger.Provides
+import dagger.multibindings.ElementsIntoSet
+
+@Module
+object KeyguardDataQuickAffordanceModule {
+    @Provides
+    @ElementsIntoSet
+    fun quickAffordanceConfigs(
+        home: HomeControlsKeyguardQuickAffordanceConfig,
+        quickAccessWallet: QuickAccessWalletKeyguardQuickAffordanceConfig,
+        qrCodeScanner: QrCodeScannerKeyguardQuickAffordanceConfig,
+    ): Set<KeyguardQuickAffordanceConfig> {
+        return setOf(
+            home,
+            quickAccessWallet,
+            qrCodeScanner,
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
index 0a8090b..fd40d1d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceConfig.kt
@@ -29,6 +29,10 @@
     /** Unique identifier for this quick affordance. It must be globally unique. */
     val key: String
 
+    val pickerName: String
+
+    val pickerIconResourceId: Int
+
     /**
      * The ever-changing state of the affordance.
      *
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt
new file mode 100644
index 0000000..9c9354f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManager.kt
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import com.android.systemui.dagger.SysUISingleton
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * Manages and provides access to the current "selections" of keyguard quick affordances, answering
+ * the question "which affordances should the keyguard show?".
+ */
+@SysUISingleton
+class KeyguardQuickAffordanceSelectionManager @Inject constructor() {
+
+    // TODO(b/254858695): implement a persistence layer (database).
+    private val _selections = MutableStateFlow<Map<String, List<String>>>(emptyMap())
+
+    /** IDs of affordances to show, indexed by slot ID, and sorted in descending priority order. */
+    val selections: Flow<Map<String, List<String>>> = _selections.asStateFlow()
+
+    /**
+     * Returns a snapshot of the IDs of affordances to show, indexed by slot ID, and sorted in
+     * descending priority order.
+     */
+    suspend fun getSelections(): Map<String, List<String>> {
+        return _selections.value
+    }
+
+    /**
+     * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance
+     * IDs should be descending priority order.
+     */
+    suspend fun setSelections(
+        slotId: String,
+        affordanceIds: List<String>,
+    ) {
+        // Must make a copy of the map and update it, otherwise, the MutableStateFlow won't emit
+        // when we set its value to the same instance of the original map, even if we change the
+        // map by updating the value of one of its keys.
+        val copy = _selections.value.toMutableMap()
+        copy[slotId] = affordanceIds
+        _selections.value = copy
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
index d620b2a..11f72ff 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfig.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.data.quickaffordance
 
+import android.content.Context
 import com.android.systemui.R
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
@@ -24,6 +25,7 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.qrcodescanner.controller.QRCodeScannerController
 import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
@@ -34,11 +36,16 @@
 class QrCodeScannerKeyguardQuickAffordanceConfig
 @Inject
 constructor(
+    @Application context: Context,
     private val controller: QRCodeScannerController,
 ) : KeyguardQuickAffordanceConfig {
 
     override val key: String = BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
 
+    override val pickerName = context.getString(R.string.qr_code_scanner_title)
+
+    override val pickerIconResourceId = R.drawable.ic_qr_code_scanner
+
     override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
         conflatedCallbackFlow {
             val callback =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
index be57a32..303e6a1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfig.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.data.quickaffordance
 
+import android.content.Context
 import android.graphics.drawable.Drawable
 import android.service.quickaccesswallet.GetWalletCardsError
 import android.service.quickaccesswallet.GetWalletCardsResponse
@@ -29,6 +30,7 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.wallet.controller.QuickAccessWalletController
 import javax.inject.Inject
@@ -40,12 +42,17 @@
 class QuickAccessWalletKeyguardQuickAffordanceConfig
 @Inject
 constructor(
+    @Application context: Context,
     private val walletController: QuickAccessWalletController,
     private val activityStarter: ActivityStarter,
 ) : KeyguardQuickAffordanceConfig {
 
     override val key: String = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
 
+    override val pickerName = context.getString(R.string.accessibility_wallet_button)
+
+    override val pickerIconResourceId = R.drawable.ic_wallet_lockscreen
+
     override val lockScreenState: Flow<KeyguardQuickAffordanceConfig.LockScreenState> =
         conflatedCallbackFlow {
             val callback =
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
index 543389e..0046256 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepository.kt
@@ -21,10 +21,9 @@
 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 com.android.systemui.statusbar.phone.KeyguardBouncer
 import javax.inject.Inject
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asStateFlow
@@ -41,9 +40,15 @@
     /** 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()
+    /**
+     * Set how much of the panel is showing on the screen.
+     * ```
+     *      0f = panel fully hidden = bouncer fully showing
+     *      1f = panel fully showing = bouncer fully hidden
+     * ```
+     */
+    private val _panelExpansionAmount = MutableStateFlow(KeyguardBouncer.EXPANSION_HIDDEN)
+    val panelExpansionAmount = _panelExpansionAmount.asStateFlow()
     private val _isVisible = MutableStateFlow(false)
     val isVisible = _isVisible.asStateFlow()
     private val _show = MutableStateFlow<KeyguardBouncerModel?>(null)
@@ -54,8 +59,6 @@
     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)
@@ -96,8 +99,8 @@
         _isScrimmed.value = isScrimmed
     }
 
-    fun setExpansion(expansion: Float) {
-        _expansionAmount.value = expansion
+    fun setPanelExpansion(panelExpansion: Float) {
+        _panelExpansionAmount.value = panelExpansion
     }
 
     fun setVisible(isVisible: Boolean) {
@@ -120,10 +123,6 @@
         _startingToHide.value = startingToHide
     }
 
-    fun setOnDismissAction(bouncerCallbackActionsModel: BouncerCallbackActionsModel?) {
-        _onDismissAction.value = bouncerCallbackActionsModel
-    }
-
     fun setStartDisappearAnimation(runnable: Runnable?) {
         _disappearAnimation.value = runnable
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
new file mode 100644
index 0000000..95f614f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
+import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+/** Abstracts access to application state related to keyguard quick affordances. */
+@SysUISingleton
+class KeyguardQuickAffordanceRepository
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    @Background private val backgroundDispatcher: CoroutineDispatcher,
+    private val selectionManager: KeyguardQuickAffordanceSelectionManager,
+    private val configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>,
+) {
+    /**
+     * List of [KeyguardQuickAffordanceConfig] instances of the affordances at the slot with the
+     * given ID. The configs are sorted in descending priority order.
+     */
+    val selections: StateFlow<Map<String, List<KeyguardQuickAffordanceConfig>>> =
+        selectionManager.selections
+            .map { selectionsBySlotId ->
+                selectionsBySlotId.mapValues { (_, selections) ->
+                    configs.filter { selections.contains(it.key) }
+                }
+            }
+            .stateIn(
+                scope = scope,
+                started = SharingStarted.Eagerly,
+                initialValue = emptyMap(),
+            )
+
+    /**
+     * Returns a snapshot of the [KeyguardQuickAffordanceConfig] instances of the affordances at the
+     * slot with the given ID. The configs are sorted in descending priority order.
+     */
+    suspend fun getSelections(slotId: String): List<KeyguardQuickAffordanceConfig> {
+        val selections = selectionManager.getSelections().getOrDefault(slotId, emptyList())
+        return configs.filter { selections.contains(it.key) }
+    }
+
+    /**
+     * Returns a snapshot of the IDs of the selected affordances, indexed by slot ID. The configs
+     * are sorted in descending priority order.
+     */
+    suspend fun getSelections(): Map<String, List<String>> {
+        return selectionManager.getSelections()
+    }
+
+    /**
+     * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance
+     * IDs should be descending priority order.
+     */
+    fun setSelections(
+        slotId: String,
+        affordanceIds: List<String>,
+    ) {
+        scope.launch(backgroundDispatcher) {
+            selectionManager.setSelections(
+                slotId = slotId,
+                affordanceIds = affordanceIds,
+            )
+        }
+    }
+
+    /**
+     * Returns the list of representation objects for all known affordances, regardless of what is
+     * selected. This is useful for building experiences like the picker/selector or user settings
+     * so the user can see everything that can be selected in a menu.
+     */
+    fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> {
+        return configs.map { config ->
+            KeyguardQuickAffordancePickerRepresentation(
+                id = config.key,
+                name = config.pickerName,
+                iconResourceId = config.pickerIconResourceId,
+            )
+        }
+    }
+
+    /**
+     * Returns the list of representation objects for all available slots on the keyguard. This is
+     * useful for building experiences like the picker/selector or user settings so the user can see
+     * each slot and select which affordance(s) is/are installed in each slot on the keyguard.
+     */
+    fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
+        // TODO(b/256195304): source these from a config XML file.
+        return listOf(
+            KeyguardSlotPickerRepresentation(
+                id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+            ),
+            KeyguardSlotPickerRepresentation(
+                id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+            ),
+        )
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index 6baaf5f..c867c6e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -23,9 +23,12 @@
 import com.android.systemui.doze.DozeHost
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.WakefulnessLifecycle.Wakefulness
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.phone.BiometricUnlockController
+import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
@@ -65,6 +68,9 @@
      */
     val isKeyguardShowing: Flow<Boolean>
 
+    /** Observable for whether the bouncer is showing. */
+    val isBouncerShowing: Flow<Boolean>
+
     /**
      * Observable for whether we are in doze state.
      *
@@ -95,6 +101,9 @@
     /** Observable for device wake/sleep state */
     val wakefulnessState: Flow<WakefulnessModel>
 
+    /** Observable for biometric unlock modes */
+    val biometricUnlockState: Flow<BiometricUnlockModel>
+
     /**
      * Returns `true` if the keyguard is showing; `false` otherwise.
      *
@@ -125,6 +134,7 @@
     private val keyguardStateController: KeyguardStateController,
     dozeHost: DozeHost,
     wakefulnessLifecycle: WakefulnessLifecycle,
+    biometricUnlockController: BiometricUnlockController,
 ) : KeyguardRepository {
     private val _animateBottomAreaDozingTransitions = MutableStateFlow(false)
     override val animateBottomAreaDozingTransitions =
@@ -159,6 +169,29 @@
         awaitClose { keyguardStateController.removeCallback(callback) }
     }
 
+    override val isBouncerShowing: Flow<Boolean> = conflatedCallbackFlow {
+        val callback =
+            object : KeyguardStateController.Callback {
+                override fun onBouncerShowingChanged() {
+                    trySendWithFailureLogging(
+                        keyguardStateController.isBouncerShowing,
+                        TAG,
+                        "updated isBouncerShowing"
+                    )
+                }
+            }
+
+        keyguardStateController.addCallback(callback)
+        // Adding the callback does not send an initial update.
+        trySendWithFailureLogging(
+            keyguardStateController.isBouncerShowing,
+            TAG,
+            "initial isBouncerShowing"
+        )
+
+        awaitClose { keyguardStateController.removeCallback(callback) }
+    }
+
     override val isDozing: Flow<Boolean> =
         conflatedCallbackFlow {
                 val callback =
@@ -248,6 +281,24 @@
         awaitClose { wakefulnessLifecycle.removeObserver(callback) }
     }
 
+    override val biometricUnlockState: Flow<BiometricUnlockModel> = conflatedCallbackFlow {
+        val callback =
+            object : BiometricUnlockController.BiometricModeListener {
+                override fun onModeChanged(@WakeAndUnlockMode mode: Int) {
+                    trySendWithFailureLogging(biometricModeIntToObject(mode), TAG, "biometric mode")
+                }
+            }
+
+        biometricUnlockController.addBiometricModeListener(callback)
+        trySendWithFailureLogging(
+            biometricModeIntToObject(biometricUnlockController.getMode()),
+            TAG,
+            "initial biometric mode"
+        )
+
+        awaitClose { biometricUnlockController.removeBiometricModeListener(callback) }
+    }
+
     override fun setAnimateDozingTransitions(animate: Boolean) {
         _animateBottomAreaDozingTransitions.value = animate
     }
@@ -279,6 +330,20 @@
         }
     }
 
+    private fun biometricModeIntToObject(@WakeAndUnlockMode value: Int): BiometricUnlockModel {
+        return when (value) {
+            0 -> BiometricUnlockModel.NONE
+            1 -> BiometricUnlockModel.WAKE_AND_UNLOCK
+            2 -> BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
+            3 -> BiometricUnlockModel.SHOW_BOUNCER
+            4 -> BiometricUnlockModel.ONLY_WAKE
+            5 -> BiometricUnlockModel.UNLOCK_COLLAPSING
+            6 -> BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
+            7 -> BiometricUnlockModel.DISMISS_BOUNCER
+            else -> throw IllegalArgumentException("Invalid BiometricUnlockModel value: $value")
+        }
+    }
+
     companion object {
         private const val TAG = "KeyguardRepositoryImpl"
     }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
new file mode 100644
index 0000000..7e01db3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/AodToGoneTransitionInteractor.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.WAKE_AND_UNLOCK
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionInfo
+import com.android.systemui.util.kotlin.sample
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+@SysUISingleton
+class AodToGoneTransitionInteractor
+@Inject
+constructor(
+    @Application private val scope: CoroutineScope,
+    private val keyguardInteractor: KeyguardInteractor,
+    private val keyguardTransitionRepository: KeyguardTransitionRepository,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
+) : TransitionInteractor("AOD->GONE") {
+
+    private val wakeAndUnlockModes =
+        setOf(WAKE_AND_UNLOCK, WAKE_AND_UNLOCK_FROM_DREAM, WAKE_AND_UNLOCK_PULSING)
+
+    override fun start() {
+        scope.launch {
+            keyguardInteractor.biometricUnlockState
+                .sample(keyguardTransitionInteractor.finishedKeyguardState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (biometricUnlockState, keyguardState) = pair
+                    if (
+                        keyguardState == KeyguardState.AOD &&
+                            wakeAndUnlockModes.contains(biometricUnlockState)
+                    ) {
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                name,
+                                KeyguardState.AOD,
+                                KeyguardState.GONE,
+                                getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 500L
+    }
+}
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
index 2af9318..dbb0352 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/BouncerInteractor.kt
@@ -30,7 +30,6 @@
 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
@@ -40,6 +39,7 @@
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.map
@@ -77,7 +77,7 @@
             KeyguardBouncerModel(
                 promptReason = repository.bouncerPromptReason ?: 0,
                 errorMessage = repository.bouncerErrorMessage,
-                expansionAmount = repository.expansionAmount.value
+                expansionAmount = repository.panelExpansionAmount.value
             )
         )
         repository.setShowingSoon(false)
@@ -90,14 +90,22 @@
     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
+    val panelExpansionAmount: Flow<Float> = repository.panelExpansionAmount
+    /** 0f = bouncer fully hidden. 1f = bouncer fully visible. */
+    val bouncerExpansion: Flow<Float> = //
+        combine(repository.panelExpansionAmount, repository.isVisible) { expansionAmount, isVisible
+            ->
+            if (isVisible) {
+                1f - expansionAmount
+            } else {
+                0f
+            }
+        }
 
     // TODO(b/243685699): Move isScrimmed logic to data layer.
     // TODO(b/243695312): Encapsulate all of the show logic for the bouncer.
@@ -128,7 +136,7 @@
         Trace.beginSection("KeyguardBouncer#show")
         repository.setScrimmed(isScrimmed)
         if (isScrimmed) {
-            setExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
+            setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
         }
 
         if (resumeBouncer) {
@@ -149,7 +157,6 @@
         }
         keyguardStateController.notifyBouncerShowing(true)
         callbackInteractor.dispatchStartingToShow()
-
         Trace.endSection()
     }
 
@@ -168,7 +175,6 @@
         keyguardStateController.notifyBouncerShowing(false /* showing */)
         cancelShowRunnable()
         repository.setShowingSoon(false)
-        repository.setOnDismissAction(null)
         repository.setVisible(false)
         repository.setHide(true)
         repository.setShow(null)
@@ -176,14 +182,17 @@
     }
 
     /**
-     * Sets the panel expansion which is calculated further upstream. Expansion is from 0f to 1f
-     * where 0f => showing and 1f => hiding
+     * Sets the panel expansion which is calculated further upstream. Panel expansion is from 0f
+     * (panel fully hidden) to 1f (panel fully showing). As the panel shows (from 0f => 1f), the
+     * bouncer hides and as the panel becomes hidden (1f => 0f), the bouncer starts to show.
+     * Therefore, a panel expansion of 1f represents the bouncer fully hidden and a panel expansion
+     * of 0f represents the bouncer fully showing.
      */
-    fun setExpansion(expansion: Float) {
-        val oldExpansion = repository.expansionAmount.value
+    fun setPanelExpansion(expansion: Float) {
+        val oldExpansion = repository.panelExpansionAmount.value
         val expansionChanged = oldExpansion != expansion
         if (repository.startingDisappearAnimation.value == null) {
-            repository.setExpansion(expansion)
+            repository.setPanelExpansion(expansion)
         }
 
         if (
@@ -227,7 +236,7 @@
         onDismissAction: ActivityStarter.OnDismissAction?,
         cancelAction: Runnable?
     ) {
-        repository.setOnDismissAction(BouncerCallbackActionsModel(onDismissAction, cancelAction))
+        bouncerView.delegate?.setDismissAction(onDismissAction, cancelAction)
     }
 
     /** Update the resources of the views. */
@@ -282,7 +291,7 @@
     /** Returns whether bouncer is fully showing. */
     fun isFullyShowing(): Boolean {
         return (repository.showingSoon.value || repository.isVisible.value) &&
-            repository.expansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE &&
+            repository.panelExpansionAmount.value == KeyguardBouncer.EXPANSION_VISIBLE &&
             repository.startingDisappearAnimation.value == null
     }
 
@@ -294,8 +303,8 @@
     /** 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
+            repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_HIDDEN &&
+                repository.panelExpansionAmount.value != KeyguardBouncer.EXPANSION_VISIBLE
     }
 
     /** Return whether bouncer is animating away. */
@@ -305,7 +314,7 @@
 
     /** Return whether bouncer will dismiss with actions */
     fun willDismissWithAction(): Boolean {
-        return repository.onDismissAction.value?.onDismissAction != null
+        return bouncerView.delegate?.willDismissWithActions() == true
     }
 
     /** Returns whether the bouncer should be full screen. */
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 03c6a78..614ff8d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -19,6 +19,8 @@
 
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
+import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
@@ -39,10 +41,19 @@
     val dozeAmount: Flow<Float> = repository.dozeAmount
     /** Whether the system is in doze mode. */
     val isDozing: Flow<Boolean> = repository.isDozing
-    /** Whether the keyguard is showing to not. */
+    /** Whether the keyguard is showing or not. */
     val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing
+    /** Whether the bouncer is showing or not. */
+    val isBouncerShowing: Flow<Boolean> = repository.isBouncerShowing
     /** The device wake/sleep state */
     val wakefulnessState: Flow<WakefulnessModel> = repository.wakefulnessState
+    /** Observable for the [StatusBarState] */
+    val statusBarState: Flow<StatusBarState> = repository.statusBarState
+    /**
+     * Observable for [BiometricUnlockModel] when biometrics like face or any fingerprint (rear,
+     * side, under display) is used to unlock the device.
+     */
+    val biometricUnlockState: Flow<BiometricUnlockModel> = repository.biometricUnlockState
 
     fun isKeyguardShowing(): Boolean {
         return repository.isKeyguardShowing()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
index 13d97aa..92caa89 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt
@@ -18,19 +18,30 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.content.Intent
+import android.util.Log
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.animation.Expandable
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
 import com.android.systemui.keyguard.domain.quickaffordance.KeyguardQuickAffordanceRegistry
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
+import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.KeyguardStateController
+import dagger.Lazy
 import javax.inject.Inject
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onStart
 
 @SysUISingleton
@@ -43,7 +54,12 @@
     private val keyguardStateController: KeyguardStateController,
     private val userTracker: UserTracker,
     private val activityStarter: ActivityStarter,
+    private val featureFlags: FeatureFlags,
+    private val repository: Lazy<KeyguardQuickAffordanceRepository>,
 ) {
+    private val isUsingRepository: Boolean
+        get() = featureFlags.isEnabled(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES)
+
     /** Returns an observable for the quick affordance at the given position. */
     fun quickAffordance(
         position: KeyguardQuickAffordancePosition
@@ -72,7 +88,19 @@
         configKey: String,
         expandable: Expandable?,
     ) {
-        @Suppress("UNCHECKED_CAST") val config = registry.get(configKey)
+        @Suppress("UNCHECKED_CAST")
+        val config =
+            if (isUsingRepository) {
+                val (slotId, decodedConfigKey) = configKey.decode()
+                repository.get().selections.value[slotId]?.find { it.key == decodedConfigKey }
+            } else {
+                registry.get(configKey)
+            }
+        if (config == null) {
+            Log.e(TAG, "Affordance config with key of \"$configKey\" not found!")
+            return
+        }
+
         when (val result = config.onTriggered(expandable)) {
             is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity ->
                 launchQuickAffordance(
@@ -84,28 +112,138 @@
         }
     }
 
+    /**
+     * Selects an affordance with the given ID on the slot with the given ID.
+     *
+     * @return `true` if the affordance was selected successfully; `false` otherwise.
+     */
+    suspend fun select(slotId: String, affordanceId: String): Boolean {
+        check(isUsingRepository)
+
+        val slots = repository.get().getSlotPickerRepresentations()
+        val slot = slots.find { it.id == slotId } ?: return false
+        val selections =
+            repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+        val alreadySelected = selections.remove(affordanceId)
+        if (!alreadySelected) {
+            while (selections.size > 0 && selections.size >= slot.maxSelectedAffordances) {
+                selections.removeAt(0)
+            }
+        }
+
+        selections.add(affordanceId)
+
+        repository
+            .get()
+            .setSelections(
+                slotId = slotId,
+                affordanceIds = selections,
+            )
+
+        return true
+    }
+
+    /**
+     * Unselects one or all affordances from the slot with the given ID.
+     *
+     * @param slotId The ID of the slot.
+     * @param affordanceId The ID of the affordance to remove; if `null`, removes all affordances
+     * from the slot.
+     * @return `true` if the affordance was successfully removed; `false` otherwise (for example, if
+     * the affordance was not on the slot to begin with).
+     */
+    suspend fun unselect(slotId: String, affordanceId: String?): Boolean {
+        check(isUsingRepository)
+
+        val slots = repository.get().getSlotPickerRepresentations()
+        if (slots.find { it.id == slotId } == null) {
+            return false
+        }
+
+        if (affordanceId.isNullOrEmpty()) {
+            return if (
+                repository.get().getSelections().getOrDefault(slotId, emptyList()).isEmpty()
+            ) {
+                false
+            } else {
+                repository.get().setSelections(slotId = slotId, affordanceIds = emptyList())
+                true
+            }
+        }
+
+        val selections =
+            repository.get().getSelections().getOrDefault(slotId, emptyList()).toMutableList()
+        return if (selections.remove(affordanceId)) {
+            repository
+                .get()
+                .setSelections(
+                    slotId = slotId,
+                    affordanceIds = selections,
+                )
+            true
+        } else {
+            false
+        }
+    }
+
+    /** Returns affordance IDs indexed by slot ID, for all known slots. */
+    suspend fun getSelections(): Map<String, List<String>> {
+        check(isUsingRepository)
+
+        val selections = repository.get().getSelections()
+        return repository.get().getSlotPickerRepresentations().associate { slotRepresentation ->
+            slotRepresentation.id to (selections[slotRepresentation.id] ?: emptyList())
+        }
+    }
+
     private fun quickAffordanceInternal(
         position: KeyguardQuickAffordancePosition
     ): Flow<KeyguardQuickAffordanceModel> {
-        val configs = registry.getAll(position)
+        return if (isUsingRepository) {
+            repository
+                .get()
+                .selections
+                .map { it[position.toSlotId()] ?: emptyList() }
+                .flatMapLatest { configs -> combinedConfigs(position, configs) }
+        } else {
+            combinedConfigs(position, registry.getAll(position))
+        }
+    }
+
+    private fun combinedConfigs(
+        position: KeyguardQuickAffordancePosition,
+        configs: List<KeyguardQuickAffordanceConfig>,
+    ): Flow<KeyguardQuickAffordanceModel> {
+        if (configs.isEmpty()) {
+            return flowOf(KeyguardQuickAffordanceModel.Hidden)
+        }
+
         return combine(
             configs.map { config ->
-                // We emit an initial "Hidden" value to make sure that there's always an initial
-                // value and avoid subtle bugs where the downstream isn't receiving any values
-                // because one config implementation is not emitting an initial value. For example,
-                // see b/244296596.
+                // We emit an initial "Hidden" value to make sure that there's always an
+                // initial value and avoid subtle bugs where the downstream isn't receiving
+                // any values because one config implementation is not emitting an initial
+                // value. For example, see b/244296596.
                 config.lockScreenState.onStart {
                     emit(KeyguardQuickAffordanceConfig.LockScreenState.Hidden)
                 }
             }
         ) { states ->
             val index =
-                states.indexOfFirst { it is KeyguardQuickAffordanceConfig.LockScreenState.Visible }
+                states.indexOfFirst { state ->
+                    state is KeyguardQuickAffordanceConfig.LockScreenState.Visible
+                }
             if (index != -1) {
                 val visibleState =
                     states[index] as KeyguardQuickAffordanceConfig.LockScreenState.Visible
+                val configKey = configs[index].key
                 KeyguardQuickAffordanceModel.Visible(
-                    configKey = configs[index].key,
+                    configKey =
+                        if (isUsingRepository) {
+                            configKey.encode(position.toSlotId())
+                        } else {
+                            configKey
+                        },
                     icon = visibleState.icon,
                     activationState = visibleState.activationState,
                 )
@@ -145,4 +283,39 @@
             )
         }
     }
+
+    private fun KeyguardQuickAffordancePosition.toSlotId(): String {
+        return when (this) {
+            KeyguardQuickAffordancePosition.BOTTOM_START ->
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
+            KeyguardQuickAffordancePosition.BOTTOM_END ->
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END
+        }
+    }
+
+    private fun String.encode(slotId: String): String {
+        return "$slotId$DELIMITER$this"
+    }
+
+    private fun String.decode(): Pair<String, String> {
+        val splitUp = this.split(DELIMITER)
+        return Pair(splitUp[0], splitUp[1])
+    }
+
+    fun getAffordancePickerRepresentations(): List<KeyguardQuickAffordancePickerRepresentation> {
+        check(isUsingRepository)
+
+        return repository.get().getAffordancePickerRepresentations()
+    }
+
+    fun getSlotPickerRepresentations(): List<KeyguardSlotPickerRepresentation> {
+        check(isUsingRepository)
+
+        return repository.get().getSlotPickerRepresentations()
+    }
+
+    companion object {
+        private const val TAG = "KeyguardQuickAffordanceInteractor"
+        private const val DELIMITER = "::"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index 83d9432..57fb4a1 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -41,11 +41,13 @@
         }
 
         scope.launch {
-            interactor.finishedKeyguardState.collect { logger.i("Finished transition to", it) }
+            interactor.finishedKeyguardTransitionStep.collect {
+                logger.i("Finished transition", it)
+            }
         }
 
         scope.launch {
-            interactor.startedKeyguardState.collect { logger.i("Started transition to", it) }
+            interactor.startedKeyguardTransitionStep.collect { logger.i("Started transition", it) }
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
index d5ea77b..a7c6d44 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionCoreStartable.kt
@@ -41,6 +41,7 @@
                     is AodLockscreenTransitionInteractor -> Log.d(TAG, "Started $it")
                     is GoneAodTransitionInteractor -> Log.d(TAG, "Started $it")
                     is LockscreenGoneTransitionInteractor -> Log.d(TAG, "Started $it")
+                    is AodToGoneTransitionInteractor -> Log.d(TAG, "Started $it")
                 }
             it.start()
         }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index dffd097..749183e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -21,7 +21,6 @@
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.KeyguardState.AOD
-import com.android.systemui.keyguard.shared.model.KeyguardState.GONE
 import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.keyguard.shared.model.TransitionStep
@@ -44,8 +43,9 @@
     /** LOCKSCREEN->AOD transition information. */
     val lockscreenToAodTransition: Flow<TransitionStep> = repository.transition(LOCKSCREEN, AOD)
 
-    /** GONE->AOD information. */
-    val goneToAodTransition: Flow<TransitionStep> = repository.transition(GONE, AOD)
+    /** (any)->AOD transition information */
+    val anyStateToAodTransition: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.to == KeyguardState.AOD }
 
     /**
      * AOD<->LOCKSCREEN transition information, mapped to dozeAmount range of AOD (1f) <->
@@ -57,15 +57,15 @@
             lockscreenToAodTransition,
         )
 
+    /* The last [TransitionStep] with a [TransitionState] of FINISHED */
+    val finishedKeyguardTransitionStep: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.transitionState == TransitionState.FINISHED }
+
     /* The last completed [KeyguardState] transition */
     val finishedKeyguardState: Flow<KeyguardState> =
-        repository.transitions
-            .filter { step -> step.transitionState == TransitionState.FINISHED }
-            .map { step -> step.to }
+        finishedKeyguardTransitionStep.map { step -> step.to }
 
-    /* The last started [KeyguardState] transition */
-    val startedKeyguardState: Flow<KeyguardState> =
-        repository.transitions
-            .filter { step -> step.transitionState == TransitionState.STARTED }
-            .map { step -> step.to }
+    /* The last [TransitionStep] with a [TransitionState] of STARTED */
+    val startedKeyguardTransitionStep: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.transitionState == TransitionState.STARTED }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
index 761f3fd..fd4814d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/LockscreenBouncerTransitionInteractor.kt
@@ -16,14 +16,16 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import android.animation.ValueAnimator
+import com.android.systemui.animation.Interpolators
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.shared.model.KeyguardState
 import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED
 import com.android.systemui.keyguard.shared.model.TransitionInfo
 import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.shade.data.repository.ShadeRepository
 import com.android.systemui.util.kotlin.sample
 import java.util.UUID
@@ -38,7 +40,7 @@
 @Inject
 constructor(
     @Application private val scope: CoroutineScope,
-    private val keyguardRepository: KeyguardRepository,
+    private val keyguardInteractor: KeyguardInteractor,
     private val shadeRepository: ShadeRepository,
     private val keyguardTransitionRepository: KeyguardTransitionRepository,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor
@@ -47,12 +49,47 @@
     private var transitionId: UUID? = null
 
     override fun start() {
+        listenForDraggingUpToBouncer()
+        listenForBouncerHiding()
+    }
+
+    private fun listenForBouncerHiding() {
+        scope.launch {
+            keyguardInteractor.isBouncerShowing
+                .sample(keyguardInteractor.wakefulnessState, { a, b -> Pair(a, b) })
+                .collect { pair ->
+                    val (isBouncerShowing, wakefulnessState) = pair
+                    if (!isBouncerShowing) {
+                        val to =
+                            if (
+                                wakefulnessState == WakefulnessModel.STARTING_TO_SLEEP ||
+                                    wakefulnessState == WakefulnessModel.ASLEEP
+                            ) {
+                                KeyguardState.AOD
+                            } else {
+                                KeyguardState.LOCKSCREEN
+                            }
+                        keyguardTransitionRepository.startTransition(
+                            TransitionInfo(
+                                ownerName = name,
+                                from = KeyguardState.BOUNCER,
+                                to = to,
+                                animator = getAnimator(),
+                            )
+                        )
+                    }
+                }
+        }
+    }
+
+    /* Starts transitions when manually dragging up the bouncer from the lockscreen. */
+    private fun listenForDraggingUpToBouncer() {
         scope.launch {
             shadeRepository.shadeModel
                 .sample(
                     combine(
                         keyguardTransitionInteractor.finishedKeyguardState,
-                        keyguardRepository.statusBarState,
+                        keyguardInteractor.statusBarState,
                     ) { keyguardState, statusBarState ->
                         Pair(keyguardState, statusBarState)
                     },
@@ -100,4 +137,15 @@
                 }
         }
     }
+
+    private fun getAnimator(): ValueAnimator {
+        return ValueAnimator().apply {
+            setInterpolator(Interpolators.LINEAR)
+            setDuration(TRANSITION_DURATION_MS)
+        }
+    }
+
+    companion object {
+        private const val TRANSITION_DURATION_MS = 300L
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
index 728bafa..37f33af 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/StartKeyguardTransitionModule.kt
@@ -42,6 +42,8 @@
 
     @Binds @IntoSet abstract fun goneAod(impl: GoneAodTransitionInteractor): TransitionInteractor
 
+    @Binds @IntoSet abstract fun aodGone(impl: AodToGoneTransitionInteractor): TransitionInteractor
+
     @Binds
     @IntoSet
     abstract fun lockscreenGone(impl: LockscreenGoneTransitionInteractor): TransitionInteractor
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockModel.kt
new file mode 100644
index 0000000..db709b4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/BiometricUnlockModel.kt
@@ -0,0 +1,50 @@
+/*
+ * 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
+
+/** Model device wakefulness states. */
+enum class BiometricUnlockModel {
+    /** Mode in which we don't need to wake up the device when we authenticate. */
+    NONE,
+    /**
+     * Mode in which we wake up the device, and directly dismiss Keyguard. Active when we acquire a
+     * fingerprint while the screen is off and the device was sleeping.
+     */
+    WAKE_AND_UNLOCK,
+    /**
+     * Mode in which we wake the device up, and fade out the Keyguard contents because they were
+     * already visible while pulsing in doze mode.
+     */
+    WAKE_AND_UNLOCK_PULSING,
+    /**
+     * Mode in which we wake up the device, but play the normal dismiss animation. Active when we
+     * acquire a fingerprint pulsing in doze mode.
+     */
+    SHOW_BOUNCER,
+    /**
+     * Mode in which we only wake up the device, and keyguard was not showing when we authenticated.
+     */
+    ONLY_WAKE,
+    /**
+     * Mode in which fingerprint unlocks the device or passive auth (ie face auth) unlocks the
+     * device while being requested when keyguard is occluded or showing.
+     */
+    UNLOCK_COLLAPSING,
+    /** When bouncer is visible and will be dismissed. */
+    DISMISS_BOUNCER,
+    /** Mode in which fingerprint wakes and unlocks the device from a dream. */
+    WAKE_AND_UNLOCK_FROM_DREAM,
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
new file mode 100644
index 0000000..a56bc90
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardQuickAffordancePickerRepresentation.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.shared.model
+
+import androidx.annotation.DrawableRes
+
+/**
+ * Representation of a quick affordance for use to build "picker", "selector", or "settings"
+ * experiences.
+ */
+data class KeyguardQuickAffordancePickerRepresentation(
+    val id: String,
+    val name: String,
+    @DrawableRes val iconResourceId: Int,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt
new file mode 100644
index 0000000..86f2756
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/KeyguardSlotPickerRepresentation.kt
@@ -0,0 +1,28 @@
+/*
+ * 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
+
+/**
+ * Representation of a quick affordance slot (or position) for use to build "picker", "selector", or
+ * "settings" experiences.
+ */
+data class KeyguardSlotPickerRepresentation(
+    val id: String,
+    /** The maximum number of selected affordances that can be present on this slot. */
+    val maxSelectedAffordances: Int = 1,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
index 0ca3582..732a6f7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/TransitionStep.kt
@@ -21,10 +21,11 @@
     val to: KeyguardState = KeyguardState.NONE,
     val value: Float = 0f, // constrained [0.0, 1.0]
     val transitionState: TransitionState = TransitionState.FINISHED,
+    val ownerName: String = "",
 ) {
     constructor(
         info: TransitionInfo,
         value: Float,
         transitionState: TransitionState,
-    ) : this(info.from, info.to, value, transitionState)
+    ) : this(info.from, info.to, value, transitionState, info.ownerName)
 }
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
index df26014..a22958b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBouncerViewBinder.kt
@@ -29,6 +29,7 @@
 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.plugins.ActivityStarter
 import com.android.systemui.statusbar.phone.KeyguardBouncer.EXPANSION_VISIBLE
 import kotlinx.coroutines.awaitCancellation
 import kotlinx.coroutines.flow.collect
@@ -75,6 +76,17 @@
                     hostViewController.showPrimarySecurityScreen()
                     hostViewController.onResume()
                 }
+
+                override fun setDismissAction(
+                    onDismissAction: ActivityStarter.OnDismissAction?,
+                    cancelAction: Runnable?
+                ) {
+                    hostViewController.setOnDismissAction(onDismissAction, cancelAction)
+                }
+
+                override fun willDismissWithActions(): Boolean {
+                    return hostViewController.hasDismissActions()
+                }
             }
         view.repeatWhenAttached {
             repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -122,15 +134,6 @@
                     }
 
                     launch {
-                        viewModel.setDismissAction.collect {
-                            hostViewController.setOnDismissAction(
-                                it.onDismissAction,
-                                it.cancelAction
-                            )
-                        }
-                    }
-
-                    launch {
                         viewModel.startDisappearAnimation.collect {
                             hostViewController.startDisappearAnimation(it)
                         }
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
index 9ad5211..9a92843 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModel.kt
@@ -20,7 +20,6 @@
 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
@@ -38,7 +37,7 @@
     private val interactor: BouncerInteractor,
 ) {
     /** Observe on bouncer expansion amount. */
-    val bouncerExpansionAmount: Flow<Float> = interactor.expansionAmount
+    val bouncerExpansionAmount: Flow<Float> = interactor.panelExpansionAmount
 
     /** Observe on bouncer visibility. */
     val isBouncerVisible: Flow<Boolean> = interactor.isVisible
@@ -63,9 +62,6 @@
     /** 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
 
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
index c089511..85d15dc 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBar.java
@@ -85,7 +85,11 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
 import android.view.View;
+import android.view.ViewRootImpl;
+import android.view.ViewRootImpl.SurfaceChangedCallback;
 import android.view.ViewTreeObserver;
 import android.view.ViewTreeObserver.InternalInsetsInfo;
 import android.view.ViewTreeObserver.OnComputeInternalInsetsListener;
@@ -354,15 +358,6 @@
         }
 
         @Override
-        public void onQuickStepStarted() {
-            // Use navbar dragging as a signal to hide the rotate button
-            mView.getRotationButtonController().setRotateSuggestionButtonState(false);
-
-            // Hide the notifications panel when quick step starts
-            mShadeController.collapsePanel(true /* animate */);
-        }
-
-        @Override
         public void onPrioritizedRotation(@Surface.Rotation int rotation) {
             mStartingQuickSwitchRotation = rotation;
             if (rotation == -1) {
@@ -475,6 +470,24 @@
                 }
             };
 
+    private final ViewRootImpl.SurfaceChangedCallback mSurfaceChangedCallback =
+            new SurfaceChangedCallback() {
+            @Override
+            public void surfaceCreated(Transaction t) {
+                notifyNavigationBarSurface();
+            }
+
+            @Override
+            public void surfaceDestroyed() {
+                notifyNavigationBarSurface();
+            }
+
+            @Override
+            public void surfaceReplaced(Transaction t) {
+                notifyNavigationBarSurface();
+            }
+    };
+
     @Inject
     NavigationBar(
             NavigationBarView navigationBarView,
@@ -680,7 +693,8 @@
         final Display display = mView.getDisplay();
         mView.setComponents(mRecentsOptional);
         if (mCentralSurfacesOptionalLazy.get().isPresent()) {
-            mView.setComponents(mCentralSurfacesOptionalLazy.get().get().getPanelController());
+            mView.setComponents(
+                    mCentralSurfacesOptionalLazy.get().get().getNotificationPanelViewController());
         }
         mView.setDisabledFlags(mDisabledFlags1, mSysUiFlagsContainer);
         mView.setOnVerticalChangedListener(this::onVerticalChanged);
@@ -701,6 +715,8 @@
 
         mView.getViewTreeObserver().addOnComputeInternalInsetsListener(
                 mOnComputeInternalInsetsListener);
+        mView.getViewRootImpl().addSurfaceChangedCallback(mSurfaceChangedCallback);
+        notifyNavigationBarSurface();
 
         mNavBarHelper.registerNavTaskStateUpdater(mNavbarTaskbarStateUpdater);
 
@@ -779,6 +795,10 @@
         mHandler.removeCallbacks(mEnableLayoutTransitions);
         mNavBarHelper.removeNavTaskStateUpdater(mNavbarTaskbarStateUpdater);
         mPipOptional.ifPresent(mView::removePipExclusionBoundsChangeListener);
+        ViewRootImpl viewRoot = mView.getViewRootImpl();
+        if (viewRoot != null) {
+            viewRoot.removeSurfaceChangedCallback(mSurfaceChangedCallback);
+        }
         mFrame = null;
         mOrientationHandle = null;
     }
@@ -932,6 +952,12 @@
         }
     }
 
+    private void notifyNavigationBarSurface() {
+        ViewRootImpl viewRoot = mView.getViewRootImpl();
+        SurfaceControl surface = viewRoot != null ? viewRoot.getSurfaceControl() : null;
+        mOverviewProxyService.onNavigationBarSurfaceChanged(surface);
+    }
+
     private int deltaRotation(int oldRotation, int newRotation) {
         int delta = newRotation - oldRotation;
         if (delta < 0) delta += 4;
@@ -1257,8 +1283,8 @@
     }
 
     private void onVerticalChanged(boolean isVertical) {
-        mCentralSurfacesOptionalLazy.get().ifPresent(
-                statusBar -> statusBar.setQsScrimEnabled(!isVertical));
+        mCentralSurfacesOptionalLazy.get().ifPresent(statusBar ->
+                statusBar.getNotificationPanelViewController().setQsScrimEnabled(!isVertical));
     }
 
     private boolean onNavigationTouch(View v, MotionEvent event) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
index 622f5a2..83c2a5d 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/buttons/KeyButtonView.java
@@ -412,10 +412,6 @@
         logSomePresses(action, flags);
         if (mCode == KeyEvent.KEYCODE_BACK && flags != KeyEvent.FLAG_LONG_PRESS) {
             Log.i(TAG, "Back button event: " + KeyEvent.actionToString(action));
-            if (action == MotionEvent.ACTION_UP) {
-                mOverviewProxyService.notifyBackAction((flags & KeyEvent.FLAG_CANCELED) == 0,
-                        -1, -1, true /* isButton */, false /* gestureSwipeLeft */);
-            }
         }
         final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0;
         final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount,
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 709467f..c319a82 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -287,8 +287,6 @@
                         mBackAnimation.setTriggerBack(true);
                     }
 
-                    mOverviewProxyService.notifyBackAction(true, (int) mDownPoint.x,
-                            (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge);
                     logGesture(mInRejectedExclusion
                             ? SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED_REJECTED
                             : SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED);
@@ -300,8 +298,6 @@
                         mBackAnimation.setTriggerBack(false);
                     }
                     logGesture(SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE);
-                    mOverviewProxyService.notifyBackAction(false, (int) mDownPoint.x,
-                            (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge);
                 }
 
                 @Override
@@ -785,9 +781,6 @@
 
         if (mExcludeRegion.contains(x, y)) {
             if (withinRange) {
-                // Log as exclusion only if it is in acceptable range in the first place.
-                mOverviewProxyService.notifyBackAction(
-                        false /* completed */, -1, -1, false /* isButton */, !mIsOnLeftEdge);
                 // We don't have the end point for logging purposes.
                 mEndPoint.x = -1;
                 mEndPoint.y = -1;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index e9a6c25..1f92b12 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -140,7 +140,7 @@
             iv.setTag(R.id.qs_icon_tag, icon);
             iv.setTag(R.id.qs_slash_tag, state.slash);
             iv.setPadding(0, padding, 0, padding);
-            if (d instanceof Animatable2) {
+            if (shouldAnimate && d instanceof Animatable2) {
                 Animatable2 a = (Animatable2) d;
                 a.start();
                 if (state.isTransient) {
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index 66be00d..46c4f41 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -64,6 +64,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.Surface;
+import android.view.SurfaceControl;
 import android.view.accessibility.AccessibilityManager;
 import android.view.inputmethod.InputMethodManager;
 
@@ -149,6 +150,7 @@
     private final UiEventLogger mUiEventLogger;
 
     private Region mActiveNavBarRegion;
+    private SurfaceControl mNavigationBarSurface;
 
     private IOverviewProxy mOverviewProxy;
     private int mConnectionBackoffAttempts;
@@ -190,7 +192,8 @@
                 // TODO move this logic to message queue
                 mCentralSurfacesOptionalLazy.get().ifPresent(centralSurfaces -> {
                     if (event.getActionMasked() == ACTION_DOWN) {
-                        centralSurfaces.getPanelController().startExpandLatencyTracking();
+                        centralSurfaces.getNotificationPanelViewController()
+                                        .startExpandLatencyTracking();
                     }
                     mHandler.post(() -> {
                         int action = event.getActionMasked();
@@ -217,17 +220,15 @@
         }
 
         @Override
-        public void onBackPressed() throws RemoteException {
+        public void onBackPressed() {
             verifyCallerAndClearCallingIdentityPostMain("onBackPressed", () -> {
                 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
                 sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
-
-                notifyBackAction(true, -1, -1, true, false);
             });
         }
 
         @Override
-        public void onImeSwitcherPressed() throws RemoteException {
+        public void onImeSwitcherPressed() {
             // TODO(b/204901476) We're intentionally using DEFAULT_DISPLAY for now since
             // Launcher/Taskbar isn't display aware.
             mContext.getSystemService(InputMethodManager.class)
@@ -316,12 +317,6 @@
         }
 
         @Override
-        public void notifySwipeUpGestureStarted() {
-            verifyCallerAndClearCallingIdentityPostMain("notifySwipeUpGestureStarted", () ->
-                    notifySwipeUpGestureStartedInternal());
-        }
-
-        @Override
         public void notifyPrioritizedRotation(@Surface.Rotation int rotation) {
             verifyCallerAndClearCallingIdentityPostMain("notifyPrioritizedRotation", () ->
                     notifyPrioritizedRotationInternal(rotation));
@@ -443,6 +438,7 @@
                 Log.e(TAG_OPS, "Failed to call onInitialize()", e);
             }
             dispatchNavButtonBounds();
+            dispatchNavigationBarSurface();
 
             // Force-update the systemui state flags
             updateSystemUiStateFlags();
@@ -597,11 +593,18 @@
                 .commitUpdate(mContext.getDisplayId());
     }
 
-    public void notifyBackAction(boolean completed, int downX, int downY, boolean isButton,
-            boolean gestureSwipeLeft) {
+    /**
+     * Called when the navigation bar surface is created or changed
+     */
+    public void onNavigationBarSurfaceChanged(SurfaceControl navbarSurface) {
+        mNavigationBarSurface = navbarSurface;
+        dispatchNavigationBarSurface();
+    }
+
+    private void dispatchNavigationBarSurface() {
         try {
             if (mOverviewProxy != null) {
-                mOverviewProxy.onBackAction(completed, downX, downY, isButton, gestureSwipeLeft);
+                mOverviewProxy.onNavigationBarSurface(mNavigationBarSurface);
             }
         } catch (RemoteException e) {
             Log.e(TAG_OPS, "Failed to notify back action", e);
@@ -614,7 +617,7 @@
         final NavigationBarView navBarView =
                 mNavBarControllerLazy.get().getNavigationBarView(mContext.getDisplayId());
         final NotificationPanelViewController panelController =
-                mCentralSurfacesOptionalLazy.get().get().getPanelController();
+                mCentralSurfacesOptionalLazy.get().get().getNotificationPanelViewController();
         if (SysUiState.DEBUG) {
             Log.d(TAG_OPS, "Updating sysui state flags: navBarFragment=" + navBarFragment
                     + " navBarView=" + navBarView + " panelController=" + panelController);
@@ -800,24 +803,12 @@
         }
     }
 
-    public void notifyQuickStepStarted() {
-        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
-            mConnectionCallbacks.get(i).onQuickStepStarted();
-        }
-    }
-
     private void notifyPrioritizedRotationInternal(@Surface.Rotation int rotation) {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
             mConnectionCallbacks.get(i).onPrioritizedRotation(rotation);
         }
     }
 
-    public void notifyQuickScrubStarted() {
-        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
-            mConnectionCallbacks.get(i).onQuickScrubStarted();
-        }
-    }
-
     private void notifyAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {
         for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
             mConnectionCallbacks.get(i).onAssistantProgress(progress);
@@ -836,12 +827,6 @@
         }
     }
 
-    private void notifySwipeUpGestureStartedInternal() {
-        for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
-            mConnectionCallbacks.get(i).onSwipeUpGestureStarted();
-        }
-    }
-
     public void notifyAssistantVisibilityChanged(float visibility) {
         try {
             if (mOverviewProxy != null) {
@@ -1005,23 +990,20 @@
         pw.print("  mWindowCornerRadius="); pw.println(mWindowCornerRadius);
         pw.print("  mSupportsRoundedCornersOnWindows="); pw.println(mSupportsRoundedCornersOnWindows);
         pw.print("  mActiveNavBarRegion="); pw.println(mActiveNavBarRegion);
+        pw.print("  mNavigationBarSurface="); pw.println(mNavigationBarSurface);
         pw.print("  mNavBarMode="); pw.println(mNavBarMode);
         mSysUiState.dump(pw, args);
     }
 
     public interface OverviewProxyListener {
         default void onConnectionChanged(boolean isConnected) {}
-        default void onQuickStepStarted() {}
-        default void onSwipeUpGestureStarted() {}
         default void onPrioritizedRotation(@Surface.Rotation int rotation) {}
         default void onOverviewShown(boolean fromHome) {}
-        default void onQuickScrubStarted() {}
         /** Notify the recents app (overview) is started by 3-button navigation. */
         default void onToggleRecentApps() {}
         default void onHomeRotationEnabled(boolean enabled) {}
         default void onTaskbarStatusUpdated(boolean visible, boolean stashed) {}
         default void onTaskbarAutohideSuspend(boolean suspend) {}
-        default void onSystemUiStateChanged(int sysuiStateFlags) {}
         default void onAssistantProgress(@FloatRange(from = 0.0, to = 1.0) float progress) {}
         default void onAssistantGestureCompletion(float velocity) {}
         default void startAssistant(Bundle bundle) {}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index e00f5eba..92f5c85 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -531,7 +531,7 @@
     private final NavigationBarController mNavigationBarController;
     private final int mDisplayId;
 
-    private KeyguardIndicationController mKeyguardIndicationController;
+    private final KeyguardIndicationController mKeyguardIndicationController;
     private int mHeadsUpInset;
     private boolean mHeadsUpPinnedMode;
     private boolean mAllowExpandForSmallExpansion;
@@ -743,6 +743,7 @@
             SysUiState sysUiState,
             Provider<KeyguardBottomAreaViewController> keyguardBottomAreaViewControllerProvider,
             KeyguardUnlockAnimationController keyguardUnlockAnimationController,
+            KeyguardIndicationController keyguardIndicationController,
             NotificationListContainer notificationListContainer,
             NotificationStackSizeCalculator notificationStackSizeCalculator,
             UnlockedScreenOffAnimationController unlockedScreenOffAnimationController,
@@ -779,6 +780,7 @@
 
         mResources = mView.getResources();
         mKeyguardStateController = keyguardStateController;
+        mKeyguardIndicationController = keyguardIndicationController;
         mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
         mNotificationShadeWindowController = notificationShadeWindowController;
         FlingAnimationUtils.Builder fauBuilder = flingAnimationUtilsBuilder.get();
@@ -1020,7 +1022,7 @@
         mNotificationStackScrollLayoutController.setOnEmptySpaceClickListener(
                 mOnEmptySpaceClickListener);
         addTrackingHeadsUpListener(mNotificationStackScrollLayoutController::setTrackingHeadsUp);
-        mKeyguardBottomArea = mView.findViewById(R.id.keyguard_bottom_area);
+        setKeyguardBottomArea(mView.findViewById(R.id.keyguard_bottom_area));
 
         initBottomArea();
 
@@ -1264,7 +1266,7 @@
         int index = mView.indexOfChild(mKeyguardBottomArea);
         mView.removeView(mKeyguardBottomArea);
         KeyguardBottomAreaView oldBottomArea = mKeyguardBottomArea;
-        mKeyguardBottomArea = mKeyguardBottomAreaViewControllerProvider.get().getView();
+        setKeyguardBottomArea(mKeyguardBottomAreaViewControllerProvider.get().getView());
         mKeyguardBottomArea.initFrom(oldBottomArea);
         mView.addView(mKeyguardBottomArea, index);
         initBottomArea();
@@ -1343,8 +1345,8 @@
         return mHintAnimationRunning || mUnlockedScreenOffAnimationController.isAnimationPlaying();
     }
 
-    public void setKeyguardIndicationController(KeyguardIndicationController indicationController) {
-        mKeyguardIndicationController = indicationController;
+    private void setKeyguardBottomArea(KeyguardBottomAreaView keyguardBottomArea) {
+        mKeyguardBottomArea = keyguardBottomArea;
         mKeyguardIndicationController.setIndicationArea(mKeyguardBottomArea);
     }
 
@@ -3880,6 +3882,7 @@
 
     public void setHeadsUpManager(HeadsUpManagerPhone headsUpManager) {
         mHeadsUpManager = headsUpManager;
+        mHeadsUpManager.addListener(mOnHeadsUpChangedListener);
         mHeadsUpTouchHelper = new HeadsUpTouchHelper(headsUpManager,
                 mNotificationStackScrollLayoutController.getHeadsUpCallback(),
                 NotificationPanelViewController.this);
@@ -4361,10 +4364,6 @@
         mView.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
     }
 
-    public ShadeHeadsUpChangedListener getOnHeadsUpChangedListener() {
-        return mOnHeadsUpChangedListener;
-    }
-
     public void setHeaderDebugInfo(String text) {
         if (DEBUG_DRAWABLE) mHeaderDebugInfo = text;
     }
@@ -4644,7 +4643,7 @@
                 mUpdateFlingVelocity = vel;
             }
         } else if (!mCentralSurfaces.isBouncerShowing()
-                && !mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()
+                && !mStatusBarKeyguardViewManager.isShowingAlternateAuth()
                 && !mKeyguardStateController.isKeyguardGoingAway()) {
             onEmptySpaceClick();
             onTrackingStopped(true);
@@ -5682,6 +5681,7 @@
 
         /** @see ViewGroup#onInterceptTouchEvent(MotionEvent) */
         public boolean onInterceptTouchEvent(MotionEvent event) {
+            mShadeLog.logMotionEvent(event, "NPVC onInterceptTouchEvent");
             if (SPEW_LOGCAT) {
                 Log.v(TAG,
                         "NPVC onInterceptTouchEvent (" + event.getId() + "): (" + event.getX()
@@ -5694,6 +5694,8 @@
             // Do not let touches go to shade or QS if the bouncer is visible,
             // but still let user swipe down to expand the panel, dismissing the bouncer.
             if (mCentralSurfaces.isBouncerShowing()) {
+                mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
+                        + "bouncer is showing");
                 return true;
             }
             if (mCommandQueue.panelsEnabled()
@@ -5701,15 +5703,21 @@
                     && mHeadsUpTouchHelper.onInterceptTouchEvent(event)) {
                 mMetricsLogger.count(COUNTER_PANEL_OPEN, 1);
                 mMetricsLogger.count(COUNTER_PANEL_OPEN_PEEK, 1);
+                mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
+                        + "HeadsUpTouchHelper");
                 return true;
             }
             if (!shouldQuickSettingsIntercept(mDownX, mDownY, 0)
                     && mPulseExpansionHandler.onInterceptTouchEvent(event)) {
+                mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
+                        + "PulseExpansionHandler");
                 return true;
             }
 
             if (!isFullyCollapsed() && onQsIntercept(event)) {
                 debugLog("onQsIntercept true");
+                mShadeLog.v("NotificationPanelViewController MotionEvent intercepted: "
+                        + "QsIntercept");
                 return true;
             }
             if (mInstantExpanding || !mNotificationsDragEnabled || mTouchDisabled || (mMotionAborted
@@ -5740,6 +5748,9 @@
                     if (mAnimatingOnDown && mClosing && !mHintAnimationRunning) {
                         cancelHeightAnimator();
                         mTouchSlopExceeded = true;
+                        mShadeLog.v("NotificationPanelViewController MotionEvent intercepted:"
+                                + " mAnimatingOnDown: true, mClosing: true, mHintAnimationRunning:"
+                                + " false");
                         return true;
                     }
                     mInitialExpandY = y;
@@ -5784,6 +5795,8 @@
                                 && hAbs > Math.abs(x - mInitialExpandX)) {
                             cancelHeightAnimator();
                             startExpandMotion(x, y, true /* startTracking */, mExpandedHeight);
+                            mShadeLog.v("NotificationPanelViewController MotionEvent"
+                                    + " intercepted: startExpandMotion");
                             return true;
                         }
                     }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
index 65bd58d..1e63b2d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java
@@ -284,7 +284,7 @@
                     return true;
                 }
 
-                if (mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()) {
+                if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) {
                     // capture all touches if the alt auth bouncer is showing
                     return true;
                 }
@@ -322,7 +322,7 @@
                     handled = !mService.isPulsing();
                 }
 
-                if (mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()) {
+                if (mStatusBarKeyguardViewManager.isShowingAlternateAuth()) {
                     // eat the touch
                     handled = true;
                 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
index 084b7dc..bf622c9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/PulsingGestureListener.kt
@@ -52,6 +52,7 @@
         private val centralSurfaces: CentralSurfaces,
         private val ambientDisplayConfiguration: AmbientDisplayConfiguration,
         private val statusBarStateController: StatusBarStateController,
+        private val shadeLogger: ShadeLogger,
         tunerService: TunerService,
         dumpManager: DumpManager
 ) : GestureDetector.SimpleOnGestureListener(), Dumpable {
@@ -77,18 +78,23 @@
     }
 
     override fun onSingleTapUp(e: MotionEvent): Boolean {
-        if (statusBarStateController.isDozing &&
-                singleTapEnabled &&
-                !dockManager.isDocked &&
-                !falsingManager.isProximityNear &&
-                !falsingManager.isFalseTap(LOW_PENALTY)
-        ) {
-            centralSurfaces.wakeUpIfDozing(
+        val isNotDocked = !dockManager.isDocked
+        shadeLogger.logSingleTapUp(statusBarStateController.isDozing, singleTapEnabled, isNotDocked)
+        if (statusBarStateController.isDozing && singleTapEnabled && isNotDocked) {
+            val proximityIsNotNear = !falsingManager.isProximityNear
+            val isNotAFalseTap = !falsingManager.isFalseTap(LOW_PENALTY)
+            shadeLogger.logSingleTapUpFalsingState(proximityIsNotNear, isNotAFalseTap)
+            if (proximityIsNotNear && isNotAFalseTap) {
+                shadeLogger.d("Single tap handled, requesting centralSurfaces.wakeUpIfDozing")
+                centralSurfaces.wakeUpIfDozing(
                     SystemClock.uptimeMillis(),
                     notificationShadeWindowView,
-                    "PULSING_SINGLE_TAP")
+                    "PULSING_SINGLE_TAP"
+                )
+            }
             return true
         }
+        shadeLogger.d("onSingleTapUp event ignored")
         return false
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
index f389dd9..eaf7fae 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
@@ -224,6 +224,6 @@
     }
 
     private NotificationPanelViewController getNotificationPanelViewController() {
-        return getCentralSurfaces().getPanelController();
+        return getCentralSurfaces().getNotificationPanelViewController();
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 7f1bba3..7615301 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -16,6 +16,10 @@
         buffer.log(TAG, LogLevel.VERBOSE, msg)
     }
 
+    fun d(@CompileTimeConstant msg: String) {
+        buffer.log(TAG, LogLevel.DEBUG, msg)
+    }
+
     private inline fun log(
         logLevel: LogLevel,
         initializer: LogMessage.() -> Unit,
@@ -123,4 +127,25 @@
                     "animatingQs=$long1"
         })
     }
+
+    fun logSingleTapUp(isDozing: Boolean, singleTapEnabled: Boolean, isNotDocked: Boolean) {
+        log(LogLevel.DEBUG, {
+            bool1 = isDozing
+            bool2 = singleTapEnabled
+            bool3 = isNotDocked
+        }, {
+            "PulsingGestureListener#onSingleTapUp all of this must true for single " +
+              "tap to be detected: isDozing: $bool1, singleTapEnabled: $bool2, isNotDocked: $bool3"
+        })
+    }
+
+    fun logSingleTapUpFalsingState(proximityIsNotNear: Boolean, isNotFalseTap: Boolean) {
+        log(LogLevel.DEBUG, {
+            bool1 = proximityIsNotNear
+            bool2 = isNotFalseTap
+        }, {
+            "PulsingGestureListener#onSingleTapUp all of this must true for single " +
+                    "tap to be detected: proximityIsNotNear: $bool1, isNotFalseTap: $bool2"
+        })
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
index 258f9fc..c070fcc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/VibratorHelper.java
@@ -19,6 +19,7 @@
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.media.AudioAttributes;
+import android.os.Process;
 import android.os.VibrationAttributes;
 import android.os.VibrationEffect;
 import android.os.Vibrator;
@@ -33,6 +34,7 @@
 import javax.inject.Inject;
 
 /**
+ *
  */
 @SysUISingleton
 public class VibratorHelper {
@@ -40,9 +42,18 @@
     private final Vibrator mVibrator;
     public static final VibrationAttributes TOUCH_VIBRATION_ATTRIBUTES =
             VibrationAttributes.createForUsage(VibrationAttributes.USAGE_TOUCH);
+
+    private static final VibrationEffect BIOMETRIC_SUCCESS_VIBRATION_EFFECT =
+            VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
+    private static final VibrationEffect BIOMETRIC_ERROR_VIBRATION_EFFECT =
+            VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
+    private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
+            VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
+
     private final Executor mExecutor;
 
     /**
+     *
      */
     @Inject
     public VibratorHelper(@Nullable Vibrator vibrator, @Background Executor executor) {
@@ -109,4 +120,23 @@
         }
         mExecutor.execute(mVibrator::cancel);
     }
+
+    /**
+     * Perform vibration when biometric authentication success
+     */
+    public void vibrateAuthSuccess(String reason) {
+        vibrate(Process.myUid(),
+                "com.android.systemui",
+                BIOMETRIC_SUCCESS_VIBRATION_EFFECT, reason,
+                HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
+    }
+
+    /**
+     * Perform vibration when biometric authentication error
+     */
+    public void vibrateAuthError(String reason) {
+        vibrate(Process.myUid(), "com.android.systemui",
+                BIOMETRIC_ERROR_VIBRATION_EFFECT, reason,
+                HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index c4ef28e..2c3330e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -114,6 +114,8 @@
 import com.android.systemui.util.DumpUtilsKt;
 import com.android.systemui.util.LargeScreenUtils;
 
+import com.google.errorprone.annotations.CompileTimeConstant;
+
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.util.ArrayList;
@@ -3693,6 +3695,8 @@
 
     @ShadeViewRefactor(RefactorComponent.INPUT)
     void handleEmptySpaceClick(MotionEvent ev) {
+        logEmptySpaceClick(ev, isBelowLastNotification(mInitialTouchX, mInitialTouchY),
+                mStatusBarState, mTouchIsClick);
         switch (ev.getActionMasked()) {
             case MotionEvent.ACTION_MOVE:
                 final float touchSlop = getTouchSlop(ev);
@@ -3704,12 +3708,34 @@
             case MotionEvent.ACTION_UP:
                 if (mStatusBarState != StatusBarState.KEYGUARD && mTouchIsClick &&
                         isBelowLastNotification(mInitialTouchX, mInitialTouchY)) {
+                    debugLog("handleEmptySpaceClick: touch event propagated further");
                     mOnEmptySpaceClickListener.onEmptySpaceClicked(mInitialTouchX, mInitialTouchY);
                 }
                 break;
+            default:
+                debugLog("handleEmptySpaceClick: MotionEvent ignored");
         }
     }
 
+    private void debugLog(@CompileTimeConstant String s) {
+        if (mLogger == null) {
+            return;
+        }
+        mLogger.d(s);
+    }
+
+    private void logEmptySpaceClick(MotionEvent ev, boolean isTouchBelowLastNotification,
+            int statusBarState, boolean touchIsClick) {
+        if (mLogger == null) {
+            return;
+        }
+        mLogger.logEmptySpaceClick(
+                isTouchBelowLastNotification,
+                statusBarState,
+                touchIsClick,
+                MotionEvent.actionToString(ev.getActionMasked()));
+    }
+
     @ShadeViewRefactor(RefactorComponent.INPUT)
     void initDownStates(MotionEvent ev) {
         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
index 4c52db7..64dd6dc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLogger.kt
@@ -2,6 +2,7 @@
 
 import com.android.systemui.log.dagger.NotificationHeadsUpLog
 import com.android.systemui.plugins.log.LogBuffer
+import com.android.systemui.plugins.log.LogLevel.DEBUG
 import com.android.systemui.plugins.log.LogLevel.INFO
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
 import com.android.systemui.statusbar.notification.logKey
@@ -10,6 +11,7 @@
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_DISAPPEAR_CLICK
 import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout.AnimationEvent.ANIMATION_TYPE_HEADS_UP_OTHER
+import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
 class NotificationStackScrollLogger @Inject constructor(
@@ -56,6 +58,25 @@
                     "key: $str1 expected: $bool1 actual: $bool2"
         })
     }
+
+    fun d(@CompileTimeConstant msg: String) = buffer.log(TAG, DEBUG, msg)
+
+    fun logEmptySpaceClick(
+        isBelowLastNotification: Boolean,
+        statusBarState: Int,
+        touchIsClick: Boolean,
+        motionEventDesc: String
+    ) {
+        buffer.log(TAG, DEBUG, {
+            int1 = statusBarState
+            bool1 = touchIsClick
+            bool2 = isBelowLastNotification
+            str1 = motionEventDesc
+        }, {
+            "handleEmptySpaceClick: statusBarState: $int1 isTouchAClick: $bool1 " +
+                    "isTouchBelowNotification: $bool2 motionEvent: $str1"
+        })
+    }
 }
 
 private const val TAG = "NotificationStackScroll"
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
index a2798f4..6e68079 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java
@@ -27,11 +27,8 @@
 import android.metrics.LogMaker;
 import android.os.Handler;
 import android.os.PowerManager;
-import android.os.Process;
 import android.os.SystemClock;
 import android.os.Trace;
-import android.os.VibrationAttributes;
-import android.os.VibrationEffect;
 import android.util.Log;
 
 import androidx.annotation.Nullable;
@@ -70,8 +67,10 @@
 import java.io.PrintWriter;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 
 import javax.inject.Inject;
 
@@ -87,12 +86,6 @@
     private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock";
     private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl();
     private static final int UDFPS_ATTEMPTS_BEFORE_SHOW_BOUNCER = 3;
-    private static final VibrationEffect SUCCESS_VIBRATION_EFFECT =
-            VibrationEffect.get(VibrationEffect.EFFECT_CLICK);
-    private static final VibrationEffect ERROR_VIBRATION_EFFECT =
-            VibrationEffect.get(VibrationEffect.EFFECT_DOUBLE_CLICK);
-    private static final VibrationAttributes HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES =
-            VibrationAttributes.createForUsage(VibrationAttributes.USAGE_HARDWARE_FEEDBACK);
 
     @IntDef(prefix = { "MODE_" }, value = {
             MODE_NONE,
@@ -167,7 +160,6 @@
     private final NotificationShadeWindowController mNotificationShadeWindowController;
     private final SessionTracker mSessionTracker;
     private final int mConsecutiveFpFailureThreshold;
-    private final boolean mShouldVibrate;
     private int mMode;
     private BiometricSourceType mBiometricType;
     private KeyguardViewController mKeyguardViewController;
@@ -177,7 +169,7 @@
     private PendingAuthenticated mPendingAuthenticated = null;
     private boolean mHasScreenTurnedOnSinceAuthenticating;
     private boolean mFadedAwayAfterWakeAndUnlock;
-    private BiometricModeListener mBiometricModeListener;
+    private Set<BiometricModeListener> mBiometricModeListeners = new HashSet<>();
 
     private final MetricsLogger mMetricsLogger;
     private final AuthController mAuthController;
@@ -307,8 +299,6 @@
         mHandler = handler;
         mConsecutiveFpFailureThreshold = resources.getInteger(
                 R.integer.fp_consecutive_failure_time_ms);
-        mShouldVibrate = !(resources.getBoolean(
-                com.android.internal.R.bool.system_server_plays_face_haptics));
         mKeyguardBypassController = keyguardBypassController;
         mKeyguardBypassController.setUnlockController(this);
         mMetricsLogger = metricsLogger;
@@ -326,9 +316,14 @@
         mKeyguardViewController = keyguardViewController;
     }
 
-    /** Sets a {@link BiometricModeListener}. */
-    public void setBiometricModeListener(BiometricModeListener biometricModeListener) {
-        mBiometricModeListener = biometricModeListener;
+    /** Adds a {@link BiometricModeListener}. */
+    public void addBiometricModeListener(BiometricModeListener listener) {
+        mBiometricModeListeners.add(listener);
+    }
+
+    /** Removes a {@link BiometricModeListener}. */
+    public void removeBiometricModeListener(BiometricModeListener listener) {
+        mBiometricModeListeners.remove(listener);
     }
 
     private final Runnable mReleaseBiometricWakeLockRunnable = new Runnable() {
@@ -423,10 +418,10 @@
     public void startWakeAndUnlock(BiometricSourceType biometricSourceType,
                                    boolean isStrongBiometric) {
         int mode = calculateMode(biometricSourceType, isStrongBiometric);
-        if (BiometricSourceType.FACE == biometricSourceType && (mode == MODE_WAKE_AND_UNLOCK
+        if (mode == MODE_WAKE_AND_UNLOCK
                 || mode == MODE_WAKE_AND_UNLOCK_PULSING || mode == MODE_UNLOCK_COLLAPSING
-                || mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM || mode == MODE_DISMISS_BOUNCER)) {
-            vibrateSuccess();
+                || mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM || mode == MODE_DISMISS_BOUNCER) {
+            vibrateSuccess(biometricSourceType);
         }
         startWakeAndUnlock(mode);
     }
@@ -511,15 +506,12 @@
                 break;
         }
         onModeChanged(mMode);
-        if (mBiometricModeListener != null) {
-            mBiometricModeListener.notifyBiometricAuthModeChanged();
-        }
         Trace.endSection();
     }
 
     private void onModeChanged(@WakeAndUnlockMode int mode) {
-        if (mBiometricModeListener != null) {
-            mBiometricModeListener.onModeChanged(mode);
+        for (BiometricModeListener listener : mBiometricModeListeners) {
+            listener.onModeChanged(mode);
         }
     }
 
@@ -659,10 +651,11 @@
         }
 
         // Suppress all face auth errors if fingerprint can be used to authenticate
-        if (biometricSourceType == BiometricSourceType.FACE
+        if ((biometricSourceType == BiometricSourceType.FACE
                 && !mUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(
-                KeyguardUpdateMonitor.getCurrentUser())) {
-            vibrateError();
+                KeyguardUpdateMonitor.getCurrentUser()))
+                || (biometricSourceType == BiometricSourceType.FINGERPRINT)) {
+            vibrateError(biometricSourceType);
         }
 
         cleanup();
@@ -688,24 +681,15 @@
         cleanup();
     }
 
-    private void vibrateSuccess() {
-        if (mShouldVibrate) {
-            mVibratorHelper.vibrate(Process.myUid(),
-                    "com.android.systemui",
-                    SUCCESS_VIBRATION_EFFECT,
-                    getClass().getSimpleName() + "::success",
-                    HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
-        }
+    //these haptics are for device-entry only
+    private void vibrateSuccess(BiometricSourceType type) {
+        mVibratorHelper.vibrateAuthSuccess(
+                getClass().getSimpleName() + ", type =" + type + "device-entry::success");
     }
 
-    private void vibrateError() {
-        if (mShouldVibrate) {
-            mVibratorHelper.vibrate(Process.myUid(),
-                    "com.android.systemui",
-                    ERROR_VIBRATION_EFFECT,
-                    getClass().getSimpleName() + "::error",
-                    HARDWARE_FEEDBACK_VIBRATION_ATTRIBUTES);
-        }
+    private void vibrateError(BiometricSourceType type) {
+        mVibratorHelper.vibrateAuthError(
+                getClass().getSimpleName() + ", type =" + type + "device-entry::error");
     }
 
     private void cleanup() {
@@ -734,9 +718,8 @@
         mMode = MODE_NONE;
         mBiometricType = null;
         mNotificationShadeWindowController.setForceDozeBrightness(false);
-        if (mBiometricModeListener != null) {
-            mBiometricModeListener.onResetMode();
-            mBiometricModeListener.notifyBiometricAuthModeChanged();
+        for (BiometricModeListener listener : mBiometricModeListeners) {
+            listener.onResetMode();
         }
         mNumConsecutiveFpFailures = 0;
         mLastFpFailureUptimeMillis = 0;
@@ -845,10 +828,8 @@
     /** An interface to interact with the {@link BiometricUnlockController}. */
     public interface BiometricModeListener {
         /** Called when {@code mMode} is reset to {@link #MODE_NONE}. */
-        void onResetMode();
+        default void onResetMode() {}
         /** Called when {@code mMode} has changed in {@link #startWakeAndUnlock(int)}. */
-        void onModeChanged(@WakeAndUnlockMode int mode);
-        /** Called after processing {@link #onModeChanged(int)}. */
-        void notifyBiometricAuthModeChanged();
+        default void onModeChanged(@WakeAndUnlockMode int mode) {}
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
index 75b444f..dc37082 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java
@@ -196,8 +196,6 @@
 
     void collapsePanelOnMainThread();
 
-    void collapsePanelWithDuration(int duration);
-
     void togglePanel();
 
     void start();
@@ -305,9 +303,6 @@
 
     void checkBarModes();
 
-    // Called by NavigationBarFragment
-    void setQsScrimEnabled(boolean scrimEnabled);
-
     void updateBubblesVisibility();
 
     void setInteracting(int barWindow, boolean interacting);
@@ -379,8 +374,6 @@
 
     void showKeyguardImpl();
 
-    boolean isInLaunchTransition();
-
     void fadeKeyguardAfterLaunchTransition(Runnable beforeFading,
             Runnable endRunnable, Runnable cancelRunnable);
 
@@ -437,8 +430,6 @@
 
     void showPinningEscapeToast();
 
-    KeyguardBottomAreaView getKeyguardBottomAreaView();
-
     void setBouncerShowing(boolean bouncerShowing);
 
   void setBouncerShowingOverDream(boolean bouncerShowingOverDream);
@@ -505,12 +496,8 @@
 
     boolean isBouncerShowingOverDream();
 
-    void onBouncerPreHideAnimation();
-
     boolean isKeyguardSecure();
 
-    NotificationPanelViewController getPanelController();
-
     NotificationGutsManager getGutsManager();
 
     void updateNotificationPanelTouchState();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index 47a12a8..15cc086 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -483,7 +483,7 @@
     private final Lazy<BiometricUnlockController> mBiometricUnlockControllerLazy;
     private final CentralSurfacesComponent.Factory mCentralSurfacesComponentFactory;
     private final PluginManager mPluginManager;
-    private final com.android.systemui.shade.ShadeController mShadeController;
+    private final ShadeController mShadeController;
     private final InitController mInitController;
 
     private final PluginDependencyProvider mPluginDependencyProvider;
@@ -496,9 +496,9 @@
     private final StatusBarSignalPolicy mStatusBarSignalPolicy;
     private final StatusBarHideIconsForBouncerManager mStatusBarHideIconsForBouncerManager;
 
-    // expanded notifications
-    // the sliding/resizing panel within the notification window
-    protected NotificationPanelViewController mNotificationPanelViewController;
+    /** Controller for the Shade. */
+    @VisibleForTesting
+    NotificationPanelViewController mNotificationPanelViewController;
 
     // settings
     private QSPanelController mQSPanelController;
@@ -1169,7 +1169,6 @@
         initializer.initializeStatusBar(mCentralSurfacesComponent);
 
         mStatusBarTouchableRegionManager.setup(this, mNotificationShadeWindowView);
-        mHeadsUpManager.addListener(mNotificationPanelViewController.getOnHeadsUpChangedListener());
         mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
 
         createNavigationBar(result);
@@ -1178,9 +1177,6 @@
             mLockscreenWallpaper = mLockscreenWallpaperLazy.get();
         }
 
-        mNotificationPanelViewController.setKeyguardIndicationController(
-                mKeyguardIndicationController);
-
         mAmbientIndicationContainer = mNotificationShadeWindowView.findViewById(
                 R.id.ambient_indication_container);
 
@@ -1531,11 +1527,12 @@
     protected void startKeyguard() {
         Trace.beginSection("CentralSurfaces#startKeyguard");
         mBiometricUnlockController = mBiometricUnlockControllerLazy.get();
-        mBiometricUnlockController.setBiometricModeListener(
+        mBiometricUnlockController.addBiometricModeListener(
                 new BiometricUnlockController.BiometricModeListener() {
                     @Override
                     public void onResetMode() {
                         setWakeAndUnlocking(false);
+                        notifyBiometricAuthModeChanged();
                     }
 
                     @Override
@@ -1546,11 +1543,7 @@
                             case BiometricUnlockController.MODE_WAKE_AND_UNLOCK:
                                 setWakeAndUnlocking(true);
                         }
-                    }
-
-                    @Override
-                    public void notifyBiometricAuthModeChanged() {
-                        CentralSurfacesImpl.this.notifyBiometricAuthModeChanged();
+                        notifyBiometricAuthModeChanged();
                     }
 
                     private void setWakeAndUnlocking(boolean wakeAndUnlocking) {
@@ -2021,8 +2014,7 @@
     }
 
     void makeExpandedInvisible() {
-        if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible
-                + " mExpandedVisible=" + mExpandedVisible);
+        if (SPEW) Log.d(TAG, "makeExpandedInvisible: mExpandedVisible=" + mExpandedVisible);
 
         if (!mExpandedVisible || mNotificationShadeWindowView == null) {
             return;
@@ -2198,12 +2190,6 @@
         mNoAnimationOnNextBarModeChange = false;
     }
 
-    // Called by NavigationBarFragment
-    @Override
-    public void setQsScrimEnabled(boolean scrimEnabled) {
-        mNotificationPanelViewController.setQsScrimEnabled(scrimEnabled);
-    }
-
     /** Temporarily hides Bubbles if the status bar is hidden. */
     @Override
     public void updateBubblesVisibility() {
@@ -2579,14 +2565,11 @@
                                 CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL,
                                 true /* force */, true /* delayed*/);
                     } else {
-
                         // Do it after DismissAction has been processed to conserve the needed
                         // ordering.
                         mMainExecutor.execute(mShadeController::runPostCollapseRunnables);
                     }
-                } else if (CentralSurfacesImpl.this.isInLaunchTransition()
-                        && mNotificationPanelViewController.isLaunchTransitionFinished()) {
-
+                } else if (mNotificationPanelViewController.isLaunchTransitionFinished()) {
                     // We are not dismissing the shade, but the launch transition is already
                     // finished,
                     // so nobody will call readyForKeyguardDone anymore. Post it such that
@@ -3008,11 +2991,6 @@
         mPresenter.updateMediaMetaData(true /* metaDataChanged */, true);
     }
 
-    @Override
-    public boolean isInLaunchTransition() {
-        return mNotificationPanelViewController.isLaunchTransitionFinished();
-    }
-
     /**
      * Fades the content of the keyguard away after the launch transition is done.
      *
@@ -3392,12 +3370,6 @@
         }
     }
 
-    /** Collapse the panel. The collapsing will be animated for the given {@code duration}. */
-    @Override
-    public void collapsePanelWithDuration(int duration) {
-        mNotificationPanelViewController.collapseWithDuration(duration);
-    }
-
     /**
      * Updates the light reveal effect to reflect the reason we're waking or sleeping (for example,
      * from the power button).
@@ -3487,12 +3459,6 @@
         mNavigationBarController.showPinningEscapeToast(mDisplayId);
     }
 
-    //TODO(b/254875405): this should be removed.
-    @Override
-    public KeyguardBottomAreaView getKeyguardBottomAreaView() {
-        return mNotificationPanelViewController.getKeyguardBottomAreaView();
-    }
-
     protected ViewRootImpl getViewRootImpl()  {
         NotificationShadeWindowView nswv = getNotificationShadeWindowView();
         if (nswv != null) return nswv.getViewRootImpl();
@@ -4195,23 +4161,11 @@
         return mBouncerShowingOverDream;
     }
 
-    /**
-     * When {@link KeyguardBouncer} starts to be dismissed, playing its animation.
-     */
-    @Override
-    public void onBouncerPreHideAnimation() {
-        mNotificationPanelViewController.startBouncerPreHideAnimation();
-
-    }
-
     @Override
     public boolean isKeyguardSecure() {
         return mStatusBarKeyguardViewManager.isSecure();
     }
-    @Override
-    public NotificationPanelViewController getPanelController() {
-        return mNotificationPanelViewController;
-    }
+
     // End Extra BaseStatusBarMethods.
 
     @Override
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 9bb4132..b2a9509 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -64,6 +64,11 @@
     private static final String TAG = "KeyguardBouncer";
     static final long BOUNCER_FACE_DELAY = 1200;
     public static final float ALPHA_EXPANSION_THRESHOLD = 0.95f;
+    /**
+     * Values for the bouncer expansion represented as the panel expansion.
+     * Panel expansion 1f = panel fully showing = bouncer fully hidden
+     * Panel expansion 0f = panel fully hiding = bouncer fully showing
+     */
     public static final float EXPANSION_HIDDEN = 1f;
     public static final float EXPANSION_VISIBLE = 0f;
 
@@ -143,6 +148,14 @@
     }
 
     /**
+     * Get the KeyguardBouncer expansion
+     * @return 1=HIDDEN, 0=SHOWING, in between 0 and 1 means the bouncer is in transition.
+     */
+    public float getExpansion() {
+        return mExpansion;
+    }
+
+    /**
      * Enable/disable only the back button
      */
     public void setBackButtonEnabled(boolean enabled) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
index 86f6ff8..0a0ded2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -18,6 +18,7 @@
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_MOBILE_NEW;
 import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI;
+import static com.android.systemui.statusbar.phone.StatusBarIconHolder.TYPE_WIFI_NEW;
 
 import android.annotation.Nullable;
 import android.content.Context;
@@ -53,8 +54,9 @@
 import com.android.systemui.statusbar.pipeline.mobile.ui.binder.MobileIconsBinder;
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView;
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel;
+import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
 import com.android.systemui.statusbar.pipeline.wifi.ui.view.ModernStatusBarWifiView;
-import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel;
 import com.android.systemui.util.Assert;
 
 import java.util.ArrayList;
@@ -84,7 +86,18 @@
     /** */
     void setIcon(String slot, StatusBarIcon icon);
     /** */
-    void setSignalIcon(String slot, WifiIconState state);
+    void setWifiIcon(String slot, WifiIconState state);
+
+    /**
+     * Sets up a wifi icon using the new data pipeline. No effect if the wifi icon has already been
+     * set up (inflated and added to the view hierarchy).
+     *
+     * This method completely replaces {@link #setWifiIcon} with the information from the new wifi
+     * data pipeline. Icons will automatically keep their state up to date, so we don't have to
+     * worry about funneling state objects through anymore.
+     */
+    void setNewWifiIcon();
+
     /** */
     void setMobileIcons(String slot, List<MobileIconState> states);
 
@@ -151,14 +164,14 @@
                 LinearLayout linearLayout,
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                WifiViewModel wifiViewModel,
+                WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider mobileContextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(linearLayout,
                     location,
                     statusBarPipelineFlags,
-                    wifiViewModel,
+                    wifiUiAdapter,
                     mobileUiAdapter,
                     mobileContextProvider);
             mIconHPadding = mContext.getResources().getDimensionPixelSize(
@@ -218,7 +231,7 @@
         @SysUISingleton
         public static class Factory {
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
-            private final WifiViewModel mWifiViewModel;
+            private final WifiUiAdapter mWifiUiAdapter;
             private final MobileContextProvider mMobileContextProvider;
             private final MobileUiAdapter mMobileUiAdapter;
             private final DarkIconDispatcher mDarkIconDispatcher;
@@ -226,12 +239,12 @@
             @Inject
             public Factory(
                     StatusBarPipelineFlags statusBarPipelineFlags,
-                    WifiViewModel wifiViewModel,
+                    WifiUiAdapter wifiUiAdapter,
                     MobileContextProvider mobileContextProvider,
                     MobileUiAdapter mobileUiAdapter,
                     DarkIconDispatcher darkIconDispatcher) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
-                mWifiViewModel = wifiViewModel;
+                mWifiUiAdapter = wifiUiAdapter;
                 mMobileContextProvider = mobileContextProvider;
                 mMobileUiAdapter = mobileUiAdapter;
                 mDarkIconDispatcher = darkIconDispatcher;
@@ -242,7 +255,7 @@
                         group,
                         location,
                         mStatusBarPipelineFlags,
-                        mWifiViewModel,
+                        mWifiUiAdapter,
                         mMobileUiAdapter,
                         mMobileContextProvider,
                         mDarkIconDispatcher);
@@ -260,14 +273,14 @@
                 ViewGroup group,
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                WifiViewModel wifiViewModel,
+                WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider mobileContextProvider
         ) {
             super(group,
                     location,
                     statusBarPipelineFlags,
-                    wifiViewModel,
+                    wifiUiAdapter,
                     mobileUiAdapter,
                     mobileContextProvider);
         }
@@ -302,19 +315,19 @@
         @SysUISingleton
         public static class Factory {
             private final StatusBarPipelineFlags mStatusBarPipelineFlags;
-            private final WifiViewModel mWifiViewModel;
+            private final WifiUiAdapter mWifiUiAdapter;
             private final MobileContextProvider mMobileContextProvider;
             private final MobileUiAdapter mMobileUiAdapter;
 
             @Inject
             public Factory(
                     StatusBarPipelineFlags statusBarPipelineFlags,
-                    WifiViewModel wifiViewModel,
+                    WifiUiAdapter wifiUiAdapter,
                     MobileUiAdapter mobileUiAdapter,
                     MobileContextProvider mobileContextProvider
             ) {
                 mStatusBarPipelineFlags = statusBarPipelineFlags;
-                mWifiViewModel = wifiViewModel;
+                mWifiUiAdapter = wifiUiAdapter;
                 mMobileUiAdapter = mobileUiAdapter;
                 mMobileContextProvider = mobileContextProvider;
             }
@@ -324,7 +337,7 @@
                         group,
                         location,
                         mStatusBarPipelineFlags,
-                        mWifiViewModel,
+                        mWifiUiAdapter,
                         mMobileUiAdapter,
                         mMobileContextProvider);
             }
@@ -336,10 +349,9 @@
      */
     class IconManager implements DemoModeCommandReceiver {
         protected final ViewGroup mGroup;
-        private final StatusBarLocation mLocation;
         private final StatusBarPipelineFlags mStatusBarPipelineFlags;
-        private final WifiViewModel mWifiViewModel;
         private final MobileContextProvider mMobileContextProvider;
+        private final LocationBasedWifiViewModel mWifiViewModel;
         private final MobileIconsViewModel mMobileIconsViewModel;
 
         protected final Context mContext;
@@ -359,26 +371,33 @@
                 ViewGroup group,
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                WifiViewModel wifiViewModel,
+                WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider mobileContextProvider
         ) {
             mGroup = group;
-            mLocation = location;
             mStatusBarPipelineFlags = statusBarPipelineFlags;
-            mWifiViewModel = wifiViewModel;
             mMobileContextProvider = mobileContextProvider;
             mContext = group.getContext();
             mIconSize = mContext.getResources().getDimensionPixelSize(
                     com.android.internal.R.dimen.status_bar_icon_size);
 
-            if (statusBarPipelineFlags.useNewMobileIcons()) {
-                // This starts the flow for the new pipeline, and will notify us of changes
+            if (statusBarPipelineFlags.runNewMobileIconsBackend()) {
+                // This starts the flow for the new pipeline, and will notify us of changes if
+                // {@link StatusBarPipelineFlags#useNewMobileIcons} is also true.
                 mMobileIconsViewModel = mobileUiAdapter.createMobileIconsViewModel();
                 MobileIconsBinder.bind(mGroup, mMobileIconsViewModel);
             } else {
                 mMobileIconsViewModel = null;
             }
+
+            if (statusBarPipelineFlags.runNewWifiIconBackend()) {
+                // This starts the flow for the new pipeline, and will notify us of changes if
+                // {@link StatusBarPipelineFlags#useNewWifiIcon} is also true.
+                mWifiViewModel = wifiUiAdapter.bindGroup(mGroup, location);
+            } else {
+                mWifiViewModel = null;
+            }
         }
 
         public boolean isDemoable() {
@@ -429,6 +448,9 @@
                 case TYPE_WIFI:
                     return addWifiIcon(index, slot, holder.getWifiState());
 
+                case TYPE_WIFI_NEW:
+                    return addNewWifiIcon(index, slot);
+
                 case TYPE_MOBILE:
                     return addMobileIcon(index, slot, holder.getMobileState());
 
@@ -450,16 +472,13 @@
 
         @VisibleForTesting
         protected StatusIconDisplayable addWifiIcon(int index, String slot, WifiIconState state) {
-            final BaseStatusBarFrameLayout view;
             if (mStatusBarPipelineFlags.useNewWifiIcon()) {
-                view = onCreateModernStatusBarWifiView(slot);
-                // When [ModernStatusBarWifiView] is created, it will automatically apply the
-                // correct view state so we don't need to call applyWifiState.
-            } else {
-                StatusBarWifiView wifiView = onCreateStatusBarWifiView(slot);
-                wifiView.applyWifiState(state);
-                view = wifiView;
+                throw new IllegalStateException("Attempting to add a mobile icon while the new "
+                        + "icons are enabled is not supported");
             }
+
+            final StatusBarWifiView view = onCreateStatusBarWifiView(slot);
+            view.applyWifiState(state);
             mGroup.addView(view, index, onCreateLayoutParams());
 
             if (mIsInDemoMode) {
@@ -468,6 +487,17 @@
             return view;
         }
 
+        protected StatusIconDisplayable addNewWifiIcon(int index, String slot) {
+            if (!mStatusBarPipelineFlags.useNewWifiIcon()) {
+                throw new IllegalStateException("Attempting to add a wifi icon using the new"
+                        + "pipeline, but the enabled flag is false.");
+            }
+
+            ModernStatusBarWifiView view = onCreateModernStatusBarWifiView(slot);
+            mGroup.addView(view, index, onCreateLayoutParams());
+            return view;
+        }
+
         @VisibleForTesting
         protected StatusIconDisplayable addMobileIcon(
                 int index,
@@ -523,8 +553,7 @@
         }
 
         private ModernStatusBarWifiView onCreateModernStatusBarWifiView(String slot) {
-            return ModernStatusBarWifiView.constructAndBind(
-                    mContext, slot, mWifiViewModel, mLocation);
+            return ModernStatusBarWifiView.constructAndBind(mContext, slot, mWifiViewModel);
         }
 
         private StatusBarMobileView onCreateStatusBarMobileView(int subId, String slot) {
@@ -600,7 +629,8 @@
                     onSetMobileIcon(viewIndex, holder.getMobileState());
                     return;
                 case TYPE_MOBILE_NEW:
-                    // Nothing, the icon updates itself now
+                case TYPE_WIFI_NEW:
+                    // Nothing, the new icons update themselves
                     return;
                 default:
                     break;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
index 31e960a..674e574 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -195,12 +195,13 @@
         }
     }
 
-    /**
-     * Signal icons need to be handled differently, because they can be
-     * composite views
-     */
     @Override
-    public void setSignalIcon(String slot, WifiIconState state) {
+    public void setWifiIcon(String slot, WifiIconState state) {
+        if (mStatusBarPipelineFlags.useNewWifiIcon()) {
+            Log.d(TAG, "ignoring old pipeline callback because the new wifi icon is enabled");
+            return;
+        }
+
         if (state == null) {
             removeIcon(slot, 0);
             return;
@@ -216,6 +217,24 @@
         }
     }
 
+
+    @Override
+    public void setNewWifiIcon() {
+        if (!mStatusBarPipelineFlags.useNewWifiIcon()) {
+            Log.d(TAG, "ignoring new pipeline callback because the new wifi icon is disabled");
+            return;
+        }
+
+        String slot = mContext.getString(com.android.internal.R.string.status_bar_wifi);
+        StatusBarIconHolder holder = mStatusBarIconList.getIconHolder(slot, /* tag= */ 0);
+        if (holder == null) {
+            holder = StatusBarIconHolder.forNewWifiIcon();
+            setIcon(slot, holder);
+        } else {
+            // Don't have to do anything in the new world
+        }
+    }
+
     /**
      * Accept a list of MobileIconStates, which all live in the same slot(?!), and then are sorted
      * by subId. Don't worry this definitely makes sense and works.
@@ -225,7 +244,7 @@
     @Override
     public void setMobileIcons(String slot, List<MobileIconState> iconStates) {
         if (mStatusBarPipelineFlags.useNewMobileIcons()) {
-            Log.d(TAG, "ignoring old pipeline callbacks, because the new "
+            Log.d(TAG, "ignoring old pipeline callbacks, because the new mobile "
                     + "icons are enabled");
             return;
         }
@@ -251,10 +270,11 @@
     public void setNewMobileIconSubIds(List<Integer> subIds) {
         if (!mStatusBarPipelineFlags.useNewMobileIcons()) {
             Log.d(TAG, "ignoring new pipeline callback, "
-                    + "since the new icons are disabled");
+                    + "since the new mobile icons are disabled");
             return;
         }
-        Slot mobileSlot = mStatusBarIconList.getSlot("mobile");
+        String slotName = mContext.getString(com.android.internal.R.string.status_bar_mobile);
+        Slot mobileSlot = mStatusBarIconList.getSlot(slotName);
 
         Collections.reverse(subIds);
 
@@ -262,7 +282,7 @@
             StatusBarIconHolder holder = mobileSlot.getHolderForTag(subId);
             if (holder == null) {
                 holder = StatusBarIconHolder.fromSubIdForModernMobileIcon(subId);
-                setIcon("mobile", holder);
+                setIcon(slotName, holder);
             } else {
                 // Don't have to do anything in the new world
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
index 68a203e..f6c0da8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconHolder.java
@@ -51,11 +51,24 @@
     @Deprecated
     public static final int TYPE_MOBILE_NEW = 3;
 
+    /**
+     * TODO (b/238425913): address this once the new pipeline is in place
+     * This type exists so that the new wifi pipeline can be used to inform the old view system
+     * about the existence of the wifi icon. The design of the new pipeline should allow for removal
+     * of this icon holder type, and obsolete the need for this entire class.
+     *
+     * @deprecated This field only exists so the new status bar pipeline can interface with the
+     * view holder system.
+     */
+    @Deprecated
+    public static final int TYPE_WIFI_NEW = 4;
+
     @IntDef({
             TYPE_ICON,
             TYPE_WIFI,
             TYPE_MOBILE,
-            TYPE_MOBILE_NEW
+            TYPE_MOBILE_NEW,
+            TYPE_WIFI_NEW
     })
     @Retention(RetentionPolicy.SOURCE)
     @interface IconType {}
@@ -95,6 +108,13 @@
         return holder;
     }
 
+    /** Creates a new holder with for the new wifi icon. */
+    public static StatusBarIconHolder forNewWifiIcon() {
+        StatusBarIconHolder holder = new StatusBarIconHolder();
+        holder.mType = TYPE_WIFI_NEW;
+        return holder;
+    }
+
     /** */
     public static StatusBarIconHolder fromMobileIconState(MobileIconState state) {
         StatusBarIconHolder holder = new StatusBarIconHolder();
@@ -172,9 +192,10 @@
             case TYPE_MOBILE:
                 return mMobileState.visible;
             case TYPE_MOBILE_NEW:
-                //TODO (b/249790733), the new pipeline can control visibility via the ViewModel
+            case TYPE_WIFI_NEW:
+                // The new pipeline controls visibilities via the view model and view binder, so
+                // this is effectively an unused return value.
                 return true;
-
             default:
                 return true;
         }
@@ -199,7 +220,9 @@
                 break;
 
             case TYPE_MOBILE_NEW:
-                //TODO (b/249790733), the new pipeline can control visibility via the ViewModel
+            case TYPE_WIFI_NEW:
+                // The new pipeline controls visibilities via the view model and view binder, so
+                // ignore setVisible.
                 break;
         }
     }
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 ccb5d88..a00e756 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -86,8 +86,10 @@
 
 import java.io.PrintWriter;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.Set;
 
 import javax.inject.Inject;
 
@@ -166,13 +168,9 @@
 
         @Override
         public void onExpansionChanged(float expansion) {
-            if (mAlternateAuthInterceptor != null) {
-                mAlternateAuthInterceptor.setBouncerExpansionChanged(expansion);
-            }
             if (mBouncerAnimating) {
                 mCentralSurfaces.setBouncerHiddenFraction(expansion);
             }
-            updateStates();
         }
 
         @Override
@@ -184,9 +182,6 @@
             if (!isVisible) {
                 mCentralSurfaces.setBouncerHiddenFraction(KeyguardBouncer.EXPANSION_HIDDEN);
             }
-            if (mAlternateAuthInterceptor != null) {
-                mAlternateAuthInterceptor.onBouncerVisibilityChanged();
-            }
 
             /* Register predictive back callback when keyguard becomes visible, and unregister
             when it's hidden. */
@@ -252,6 +247,7 @@
     private int mLastBiometricMode;
     private boolean mLastScreenOffAnimationPlaying;
     private float mQsExpansion;
+    final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>();
     private boolean mIsModernBouncerEnabled;
 
     private OnDismissAction mAfterKeyguardGoneAction;
@@ -465,7 +461,7 @@
             if (mBouncer != null) {
                 mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
             } else {
-                mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+                mBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
             }
         } else if (mStatusBarStateController.getState() == StatusBarState.SHADE_LOCKED) {
             // Don't expand to the bouncer. Instead transition back to the lock screen (see
@@ -475,17 +471,17 @@
             if (mBouncer != null) {
                 mBouncer.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
             } else {
-                mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
+                mBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE);
             }
         } else if (mKeyguardStateController.isShowing()  && !hideBouncerOverDream) {
             if (!isWakeAndUnlocking()
                     && !(mBiometricUnlockController.getMode() == MODE_DISMISS_BOUNCER)
-                    && !mCentralSurfaces.isInLaunchTransition()
+                    && !mNotificationPanelViewController.isLaunchTransitionFinished()
                     && !isUnlockCollapsing()) {
                 if (mBouncer != null) {
                     mBouncer.setExpansion(fraction);
                 } else {
-                    mBouncerInteractor.setExpansion(fraction);
+                    mBouncerInteractor.setPanelExpansion(fraction);
                 }
             }
             if (fraction != KeyguardBouncer.EXPANSION_HIDDEN && tracking
@@ -504,7 +500,7 @@
             if (mBouncer != null) {
                 mBouncer.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
             } else {
-                mBouncerInteractor.setExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
+                mBouncerInteractor.setPanelExpansion(KeyguardBouncer.EXPANSION_HIDDEN);
             }
         } else if (mPulsing && fraction == KeyguardBouncer.EXPANSION_VISIBLE) {
             // Panel expanded while pulsing but didn't translate the bouncer (because we are
@@ -849,7 +845,7 @@
         if (isShowing && isOccluding) {
             SysUiStatsLog.write(SysUiStatsLog.KEYGUARD_STATE_CHANGED,
                     SysUiStatsLog.KEYGUARD_STATE_CHANGED__STATE__OCCLUDED);
-            if (mCentralSurfaces.isInLaunchTransition()) {
+            if (mNotificationPanelViewController.isLaunchTransitionFinished()) {
                 final Runnable endRunnable = new Runnable() {
                     @Override
                     public void run() {
@@ -903,7 +899,7 @@
             } else {
                 mBouncerInteractor.startDisappearAnimation(finishRunnable);
             }
-            mCentralSurfaces.onBouncerPreHideAnimation();
+            mNotificationPanelViewController.startBouncerPreHideAnimation();
 
             // We update the state (which will show the keyguard) only if an animation will run on
             // the keyguard. If there is no animation, we wait before updating the state so that we
@@ -935,7 +931,7 @@
         long uptimeMillis = SystemClock.uptimeMillis();
         long delay = Math.max(0, startTime + HIDE_TIMING_CORRECTION_MS - uptimeMillis);
 
-        if (mCentralSurfaces.isInLaunchTransition()
+        if (mNotificationPanelViewController.isLaunchTransitionFinished()
                 || mKeyguardStateController.isFlingingToDismissKeyguard()) {
             final boolean wasFlingingToDismissKeyguard =
                     mKeyguardStateController.isFlingingToDismissKeyguard();
@@ -1313,7 +1309,7 @@
 
     @Override
     public boolean shouldDisableWindowAnimationsForUnlock() {
-        return mCentralSurfaces.isInLaunchTransition();
+        return mNotificationPanelViewController.isLaunchTransitionFinished();
     }
 
     @Override
@@ -1356,7 +1352,7 @@
             mBouncerInteractor.notifyKeyguardAuthenticated(strongAuth);
         }
 
-        if (mAlternateAuthInterceptor != null && isShowingAlternateAuthOrAnimating()) {
+        if (mAlternateAuthInterceptor != null && isShowingAlternateAuth()) {
             resetAlternateAuth(false);
             executeAfterKeyguardGoneAction();
         }
@@ -1442,6 +1438,10 @@
         pw.println("  mPendingWakeupAction: " + mPendingWakeupAction);
         pw.println("  isBouncerShowing(): " + isBouncerShowing());
         pw.println("  bouncerIsOrWillBeShowing(): " + bouncerIsOrWillBeShowing());
+        pw.println("  Registered KeyguardViewManagerCallbacks:");
+        for (KeyguardViewManagerCallback callback : mCallbacks) {
+            pw.println("      " + callback);
+        }
 
         if (mBouncer != null) {
             mBouncer.dump(pw);
@@ -1466,6 +1466,20 @@
     }
 
     /**
+     * Add a callback to listen for changes
+     */
+    public void addCallback(KeyguardViewManagerCallback callback) {
+        mCallbacks.add(callback);
+    }
+
+    /**
+     * Removes callback to stop receiving updates
+     */
+    public void removeCallback(KeyguardViewManagerCallback callback) {
+        mCallbacks.remove(callback);
+    }
+
+    /**
      * Whether qs is currently expanded.
      */
     public float getQsExpansion() {
@@ -1477,8 +1491,8 @@
      */
     public void setQsExpansion(float qsExpansion) {
         mQsExpansion = qsExpansion;
-        if (mAlternateAuthInterceptor != null) {
-            mAlternateAuthInterceptor.setQsExpansion(qsExpansion);
+        for (KeyguardViewManagerCallback callback : mCallbacks) {
+            callback.onQSExpansionChanged(mQsExpansion);
         }
     }
 
@@ -1492,21 +1506,13 @@
                 && mAlternateAuthInterceptor.isShowingAlternateAuthBouncer();
     }
 
-    public boolean isShowingAlternateAuthOrAnimating() {
-        return mAlternateAuthInterceptor != null
-                && (mAlternateAuthInterceptor.isShowingAlternateAuthBouncer()
-                || mAlternateAuthInterceptor.isAnimating());
-    }
-
     /**
-     * Forward touches to any alternate authentication affordances.
+     * Forward touches to callbacks.
      */
-    public boolean onTouch(MotionEvent event) {
-        if (mAlternateAuthInterceptor == null) {
-            return false;
+    public void onTouch(MotionEvent event) {
+        for (KeyguardViewManagerCallback callback: mCallbacks) {
+            callback.onTouch(event);
         }
-
-        return mAlternateAuthInterceptor.onTouch(event);
     }
 
     /** Update keyguard position based on a tapped X coordinate. */
@@ -1640,38 +1646,6 @@
         boolean isShowingAlternateAuthBouncer();
 
         /**
-         * print information for the alternate auth interceptor registered
-         */
-        void dump(PrintWriter pw);
-
-        /**
-         * @return true if the new auth method bouncer is currently animating in or out.
-         */
-        boolean isAnimating();
-
-        /**
-         * How much QS is fully expanded where 0f is not showing and 1f is fully expanded.
-         */
-        void setQsExpansion(float qsExpansion);
-
-        /**
-         * Forward potential touches to authentication interceptor
-         * @return true if event was handled
-         */
-        boolean onTouch(MotionEvent event);
-
-        /**
-         * Update pin/pattern/password bouncer expansion amount where 0 is visible and 1 is fully
-         * hidden
-         */
-        void setBouncerExpansionChanged(float expansion);
-
-        /**
-         *  called when the bouncer view visibility has changed.
-         */
-        void onBouncerVisibilityChanged();
-
-        /**
          * Use when an app occluding the keyguard would like to give the user ability to
          * unlock the device using udfps.
          *
@@ -1680,5 +1654,25 @@
          */
         void requestUdfps(boolean requestUdfps, int color);
 
+        /**
+         * print information for the alternate auth interceptor registered
+         */
+        void dump(PrintWriter pw);
+    }
+
+    /**
+     * Callback for KeyguardViewManager state changes.
+     */
+    public interface KeyguardViewManagerCallback {
+        /**
+         * Set the amount qs is expanded. For example, swipe down from the top of the
+         * lock screen to start the full QS expansion.
+         */
+        default void onQSExpansionChanged(float qsExpansion) { }
+
+        /**
+         * Forward touch events to callbacks
+         */
+        default void onTouch(MotionEvent event) { }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
index ee948c0..b1642d6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarLaunchAnimatorController.kt
@@ -31,7 +31,7 @@
         delegate.onLaunchAnimationStart(isExpandingFullyAbove)
         centralSurfaces.notificationPanelViewController.setIsLaunchAnimationRunning(true)
         if (!isExpandingFullyAbove) {
-            centralSurfaces.collapsePanelWithDuration(
+            centralSurfaces.notificationPanelViewController.collapseWithDuration(
                 ActivityLaunchAnimator.TIMINGS.totalDuration.toInt())
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
index 492734e..de7bf3c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarSignalPolicy.java
@@ -212,7 +212,7 @@
     private void updateWifiIconWithState(WifiIconState state) {
         if (DEBUG) Log.d(TAG, "WifiIconState: " + state == null ? "" : state.toString());
         if (state.visible && state.resId > 0) {
-            mIconController.setSignalIcon(mSlotWifi, state);
+            mIconController.setWifiIcon(mSlotWifi, state);
             mIconController.setIconVisibility(mSlotWifi, true);
         } else {
             mIconController.setIconVisibility(mSlotWifi, false);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
index 06cd12d..946d7e4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -27,11 +27,26 @@
     /** True if we should display the mobile icons using the new status bar data pipeline. */
     fun useNewMobileIcons(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS)
 
+    /**
+     * True if we should run the new mobile icons backend to get the logging.
+     *
+     * Does *not* affect whether we render the mobile icons using the new backend data. See
+     * [useNewMobileIcons] for that.
+     */
+    fun runNewMobileIconsBackend(): Boolean =
+        featureFlags.isEnabled(Flags.NEW_STATUS_BAR_MOBILE_ICONS_BACKEND) || useNewMobileIcons()
+
     /** True if we should display the wifi icon using the new status bar data pipeline. */
     fun useNewWifiIcon(): Boolean = featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON)
 
-    // TODO(b/238425913): Add flags to only run the mobile backend or wifi backend so we get the
-    //   logging without getting the UI effects.
+    /**
+     * True if we should run the new wifi icon backend to get the logging.
+     *
+     * Does *not* affect whether we render the wifi icon using the new backend data. See
+     * [useNewWifiIcon] for that.
+     */
+    fun runNewWifiIconBackend(): Boolean =
+        featureFlags.isEnabled(Flags.NEW_STATUS_BAR_WIFI_ICON_BACKEND) || useNewWifiIcon()
 
     /**
      * Returns true if we should apply some coloring to the wifi icon that was rendered with the new
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
index 380017c..c7e0ce1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt
@@ -20,6 +20,7 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.phone.StatusBarIconController
 import com.android.systemui.statusbar.phone.StatusBarIconController.IconManager
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
 import javax.inject.Inject
@@ -50,6 +51,7 @@
     private val iconController: StatusBarIconController,
     private val iconsViewModelFactory: MobileIconsViewModel.Factory,
     @Application scope: CoroutineScope,
+    private val statusBarPipelineFlags: StatusBarPipelineFlags,
 ) {
     private val mobileSubIds: Flow<List<Int>> =
         interactor.filteredSubscriptions.mapLatest { infos ->
@@ -66,8 +68,14 @@
     private val mobileSubIdsState: StateFlow<List<Int>> =
         mobileSubIds
             .onEach {
-                // Notify the icon controller here so that it knows to add icons
-                iconController.setNewMobileIconSubIds(it)
+                // Only notify the icon controller if we want to *render* the new icons.
+                // Note that this flow may still run if
+                // [statusBarPipelineFlags.runNewMobileIconsBackend] is true because we may want to
+                // get the logging data without rendering.
+                if (statusBarPipelineFlags.useNewMobileIcons()) {
+                    // Notify the icon controller here so that it knows to add icons
+                    iconController.setNewMobileIconSubIds(it)
+                }
             }
             .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.kt
new file mode 100644
index 0000000..b816364
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/WifiUiAdapter.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.statusbar.pipeline.wifi.ui
+
+import android.view.ViewGroup
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.statusbar.phone.StatusBarIconController
+import com.android.systemui.statusbar.phone.StatusBarLocation
+import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.collect
+import kotlinx.coroutines.launch
+
+/**
+ * This class serves as a bridge between the old UI classes and the new data pipeline.
+ *
+ * Once the new pipeline notifies [wifiViewModel] that the wifi icon should be visible, this class
+ * notifies [iconController] to inflate the wifi icon (if needed). After that, the [wifiViewModel]
+ * has sole responsibility for updating the wifi icon drawable, visibility, etc. and the
+ * [iconController] will not do any updates to the icon.
+ */
+@SysUISingleton
+class WifiUiAdapter
+@Inject
+constructor(
+    private val iconController: StatusBarIconController,
+    private val wifiViewModel: WifiViewModel,
+    private val statusBarPipelineFlags: StatusBarPipelineFlags,
+) {
+    /**
+     * Binds the container for all the status bar icons to a view model, so that we inflate the wifi
+     * view once we receive a valid icon from the data pipeline.
+     *
+     * NOTE: This should go away as we better integrate the data pipeline with the UI.
+     *
+     * @return the view model used for this particular group in the given [location].
+     */
+    fun bindGroup(
+        statusBarIconGroup: ViewGroup,
+        location: StatusBarLocation,
+    ): LocationBasedWifiViewModel {
+        val locationViewModel =
+            when (location) {
+                StatusBarLocation.HOME -> wifiViewModel.home
+                StatusBarLocation.KEYGUARD -> wifiViewModel.keyguard
+                StatusBarLocation.QS -> wifiViewModel.qs
+            }
+
+        statusBarIconGroup.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                launch {
+                    locationViewModel.wifiIcon.collect { wifiIcon ->
+                        // Only notify the icon controller if we want to *render* the new icon.
+                        // Note that this flow may still run if
+                        // [statusBarPipelineFlags.runNewWifiIconBackend] is true because we may
+                        // want to get the logging data without rendering.
+                        if (wifiIcon != null && statusBarPipelineFlags.useNewWifiIcon()) {
+                            iconController.setNewWifiIcon()
+                        }
+                    }
+                }
+            }
+        }
+
+        return locationViewModel
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
index 25537b9..345f8cb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/binder/WifiViewBinder.kt
@@ -30,9 +30,7 @@
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
-import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
-import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
 import kotlinx.coroutines.InternalCoroutinesApi
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.collect
@@ -62,26 +60,9 @@
         fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int)
     }
 
-    /**
-     * Binds the view to the appropriate view-model based on the given location. The view will
-     * continue to be updated following updates from the view-model.
-     */
-    @JvmStatic
-    fun bind(
-        view: ViewGroup,
-        wifiViewModel: WifiViewModel,
-        location: StatusBarLocation,
-    ): Binding {
-        return when (location) {
-            StatusBarLocation.HOME -> bind(view, wifiViewModel.home)
-            StatusBarLocation.KEYGUARD -> bind(view, wifiViewModel.keyguard)
-            StatusBarLocation.QS -> bind(view, wifiViewModel.qs)
-        }
-    }
-
     /** Binds the view to the view-model, continuing to update the former based on the latter. */
     @JvmStatic
-    private fun bind(
+    fun bind(
         view: ViewGroup,
         viewModel: LocationBasedWifiViewModel,
     ): Binding {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
index 0cd9bd7..a45076b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiView.kt
@@ -26,9 +26,8 @@
 import com.android.systemui.statusbar.StatusBarIconView
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
-import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.wifi.ui.binder.WifiViewBinder
-import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 
 /**
  * A new and more modern implementation of [com.android.systemui.statusbar.StatusBarWifiView] that
@@ -81,12 +80,11 @@
 
     private fun initView(
         slotName: String,
-        wifiViewModel: WifiViewModel,
-        location: StatusBarLocation,
+        wifiViewModel: LocationBasedWifiViewModel,
     ) {
         slot = slotName
         initDotView()
-        binding = WifiViewBinder.bind(this, wifiViewModel, location)
+        binding = WifiViewBinder.bind(this, wifiViewModel)
     }
 
     // Mostly duplicated from [com.android.systemui.statusbar.StatusBarWifiView].
@@ -116,14 +114,13 @@
         fun constructAndBind(
             context: Context,
             slot: String,
-            wifiViewModel: WifiViewModel,
-            location: StatusBarLocation,
+            wifiViewModel: LocationBasedWifiViewModel,
         ): ModernStatusBarWifiView {
             return (
                 LayoutInflater.from(context).inflate(R.layout.new_status_bar_wifi_group, null)
                     as ModernStatusBarWifiView
                 ).also {
-                    it.initView(slot, wifiViewModel, location)
+                    it.initView(slot, wifiViewModel)
                 }
         }
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
index 89b96b7..0782bbb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/ui/viewmodel/WifiViewModel.kt
@@ -145,7 +145,8 @@
                 else -> null
             }
         }
-        .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
+                .logOutputChange(logger, "icon") { icon -> icon?.contentDescription.toString() }
+                .stateIn(scope, started = SharingStarted.WhileSubscribed(), initialValue = null)
 
     /** The wifi activity status. Null if we shouldn't display the activity status. */
     private val activity: Flow<WifiActivityModel?> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
index bd2123a..69b55c8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
@@ -33,6 +33,7 @@
 import androidx.annotation.NonNull;
 
 import com.android.internal.util.ConcurrentUtils;
+import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.dagger.qualifiers.Main;
@@ -63,6 +64,7 @@
     private volatile int mNumConnectedDevices;
     // Assume tethering is available until told otherwise
     private volatile boolean mIsTetheringSupported = true;
+    private final boolean mIsTetheringSupportedConfig;
     private volatile boolean mHasTetherableWifiRegexs = true;
     private boolean mWaitingForTerminalState;
 
@@ -100,23 +102,29 @@
         mTetheringManager = context.getSystemService(TetheringManager.class);
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
         mMainHandler = mainHandler;
-        mTetheringManager.registerTetheringEventCallback(
-                new HandlerExecutor(backgroundHandler), mTetheringCallback);
+        mIsTetheringSupportedConfig = context.getResources()
+                .getBoolean(R.bool.config_show_wifi_tethering);
+        if (mIsTetheringSupportedConfig) {
+            mTetheringManager.registerTetheringEventCallback(
+                    new HandlerExecutor(backgroundHandler), mTetheringCallback);
+        }
         dumpManager.registerDumpable(getClass().getSimpleName(), this);
     }
 
     /**
      * Whether hotspot is currently supported.
      *
-     * This will return {@code true} immediately on creation of the controller, but may be updated
-     * later. Callbacks from this controllers will notify if the state changes.
+     * This may return {@code true} immediately on creation of the controller, but may be updated
+     * later as capabilities are collected from System Server.
+     *
+     * Callbacks from this controllers will notify if the state changes.
      *
      * @return {@code true} if hotspot is supported (or we haven't been told it's not)
      * @see #addCallback
      */
     @Override
     public boolean isHotspotSupported() {
-        return mIsTetheringSupported && mHasTetherableWifiRegexs
+        return mIsTetheringSupportedConfig && mIsTetheringSupported && mHasTetherableWifiRegexs
                 && UserManager.get(mContext).isUserAdmin(ActivityManager.getCurrentUser());
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
index 07e5cf9..2080614 100644
--- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/GuestUserInteractor.kt
@@ -28,6 +28,8 @@
 import android.view.WindowManagerGlobal
 import android.widget.Toast
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.GuestResetOrExitSessionReceiver
+import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
@@ -58,6 +60,8 @@
     private val devicePolicyManager: DevicePolicyManager,
     private val refreshUsersScheduler: RefreshUsersScheduler,
     private val uiEventLogger: UiEventLogger,
+    resumeSessionReceiver: GuestResumeSessionReceiver,
+    resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver,
 ) {
     /** Whether the device is configured to always have a guest user available. */
     val isGuestUserAutoCreated: Boolean = repository.isGuestUserAutoCreated
@@ -65,6 +69,11 @@
     /** Whether the guest user is currently being reset. */
     val isGuestUserResetting: Boolean = repository.isGuestUserResetting
 
+    init {
+        resumeSessionReceiver.register()
+        resetOrExitSessionReceiver.register()
+    }
+
     /** Notifies that the device has finished booting. */
     fun onDeviceBootCompleted() {
         applicationScope.launch {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
index aca60c0..131cf7d 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardListenQueueTest.kt
@@ -72,6 +72,7 @@
     keyguardOccluded = false,
     occludingAppRequestingFp = false,
     primaryUser = false,
+    shouldListenSfpsState = false,
     shouldListenForFingerprintAssistant = false,
     switchingUser = false,
     udfps = false,
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
index 52f8ef8..0a2b3d8 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.java
@@ -194,6 +194,15 @@
     }
 
     @Test
+    public void onInitConfiguresViewMode() {
+        mKeyguardSecurityContainerController.onInit();
+        verify(mView).initMode(eq(MODE_DEFAULT), eq(mGlobalSettings), eq(mFalsingManager),
+                eq(mUserSwitcherController),
+                any(KeyguardSecurityContainer.UserSwitcherViewMode.UserSwitcherCallback.class),
+                eq(mFalsingA11yDelegate));
+    }
+
+    @Test
     public void showSecurityScreen_canInflateAllModes() {
         SecurityMode[] modes = SecurityMode.values();
         for (SecurityMode mode : modes) {
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index 5104f84..a8284d2 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -20,6 +20,7 @@
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT;
 import static android.hardware.biometrics.BiometricFingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT;
+import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
 import static android.telephony.SubscriptionManager.DATA_ROAMING_DISABLE;
 import static android.telephony.SubscriptionManager.NAME_SOURCE_CARRIER_ID;
 
@@ -38,6 +39,7 @@
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
@@ -53,6 +55,7 @@
 import android.app.trust.TrustManager;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
@@ -60,18 +63,21 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.ServiceInfo;
 import android.content.pm.UserInfo;
+import android.database.ContentObserver;
 import android.hardware.SensorPrivacyManager;
 import android.hardware.biometrics.BiometricConstants;
 import android.hardware.biometrics.BiometricManager;
 import android.hardware.biometrics.BiometricSourceType;
 import android.hardware.biometrics.ComponentInfoInternal;
 import android.hardware.biometrics.IBiometricEnabledOnKeyguardCallback;
+import android.hardware.biometrics.SensorProperties;
 import android.hardware.face.FaceManager;
 import android.hardware.face.FaceSensorProperties;
 import android.hardware.face.FaceSensorPropertiesInternal;
 import android.hardware.fingerprint.FingerprintManager;
 import android.hardware.fingerprint.FingerprintSensorProperties;
 import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
+import android.net.Uri;
 import android.nfc.NfcAdapter;
 import android.os.Bundle;
 import android.os.CancellationSignal;
@@ -110,6 +116,7 @@
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.telephony.TelephonyListenerManager;
 import com.android.systemui.util.settings.GlobalSettings;
+import com.android.systemui.util.settings.SecureSettings;
 
 import org.junit.After;
 import org.junit.Assert;
@@ -181,6 +188,8 @@
     @Mock
     private BroadcastDispatcher mBroadcastDispatcher;
     @Mock
+    private SecureSettings mSecureSettings;
+    @Mock
     private TelephonyManager mTelephonyManager;
     @Mock
     private SensorPrivacyManager mSensorPrivacyManager;
@@ -214,6 +223,7 @@
     private GlobalSettings mGlobalSettings;
     private FaceWakeUpTriggersConfig mFaceWakeUpTriggersConfig;
 
+
     private final int mCurrentUserId = 100;
     private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0);
 
@@ -223,6 +233,9 @@
     @Captor
     private ArgumentCaptor<FaceManager.AuthenticationCallback> mAuthenticationCallbackCaptor;
 
+    @Mock
+    private Uri mURI;
+
     // Direct executor
     private final Executor mBackgroundExecutor = Runnable::run;
     private final Executor mMainExecutor = Runnable::run;
@@ -305,6 +318,15 @@
 
         mTestableLooper = TestableLooper.get(this);
         allowTestableLooperAsMainThread();
+
+        when(mSecureSettings.getUriFor(anyString())).thenReturn(mURI);
+
+        final ContentResolver contentResolver = mContext.getContentResolver();
+        ExtendedMockito.spyOn(contentResolver);
+        doNothing().when(contentResolver)
+                .registerContentObserver(any(Uri.class), anyBoolean(), any(ContentObserver.class),
+                        anyInt());
+
         mKeyguardUpdateMonitor = new TestableKeyguardUpdateMonitor(mContext);
 
         verify(mBiometricManager)
@@ -1136,6 +1158,64 @@
     }
 
     @Test
+    public void testStartsListeningForSfps_whenKeyguardIsVisible_ifRequireScreenOnToAuthEnabled()
+            throws RemoteException {
+        // SFPS supported and enrolled
+        final ArrayList<FingerprintSensorPropertiesInternal> props = new ArrayList<>();
+        props.add(newFingerprintSensorPropertiesInternal(TYPE_POWER_BUTTON));
+        when(mAuthController.getSfpsProps()).thenReturn(props);
+        when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true);
+
+        // WHEN require screen on to auth is disabled, and keyguard is not awake
+        when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(0);
+        mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref();
+
+        mContext.getOrCreateTestableResources().addOverride(
+                com.android.internal.R.bool.config_requireScreenOnToAuthEnabled, true);
+
+        // Preconditions for sfps auth to run
+        keyguardNotGoingAway();
+        currentUserIsPrimary();
+        currentUserDoesNotHaveTrust();
+        biometricsNotDisabledThroughDevicePolicyManager();
+        biometricsEnabledForCurrentUser();
+        userNotCurrentlySwitching();
+
+        statusBarShadeIsLocked();
+        mTestableLooper.processAllMessages();
+
+        // THEN we should listen for sfps when screen off, because require screen on is disabled
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
+
+        // WHEN require screen on to auth is enabled, and keyguard is not awake
+        when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).thenReturn(1);
+        mKeyguardUpdateMonitor.updateSfpsRequireScreenOnToAuthPref();
+
+        // THEN we shouldn't listen for sfps when screen off, because require screen on is enabled
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isFalse();
+
+        // Device now awake & keyguard is now interactive
+        deviceNotGoingToSleep();
+        deviceIsInteractive();
+        keyguardIsVisible();
+
+        // THEN we should listen for sfps when screen on, and require screen on is enabled
+        assertThat(mKeyguardUpdateMonitor.shouldListenForFingerprint(false)).isTrue();
+    }
+
+
+    private FingerprintSensorPropertiesInternal newFingerprintSensorPropertiesInternal(
+            @FingerprintSensorProperties.SensorType int sensorType) {
+        return new FingerprintSensorPropertiesInternal(
+                0 /* sensorId */,
+                SensorProperties.STRENGTH_STRONG,
+                1 /* maxEnrollmentsPerUser */,
+                new ArrayList<ComponentInfoInternal>(),
+                sensorType,
+                true /* resetLockoutRequiresHardwareAuthToken */);
+    }
+
+    @Test
     public void testShouldNotListenForUdfps_whenTrustEnabled() {
         // GIVEN a "we should listen for udfps" state
         mStatusBarStateListener.onStateChanged(StatusBarState.KEYGUARD);
@@ -1804,7 +1884,7 @@
         protected TestableKeyguardUpdateMonitor(Context context) {
             super(context,
                     TestableLooper.get(KeyguardUpdateMonitorTest.this).getLooper(),
-                    mBroadcastDispatcher, mDumpManager,
+                    mBroadcastDispatcher, mSecureSettings, mDumpManager,
                     mBackgroundExecutor, mMainExecutor,
                     mStatusBarStateController, mLockPatternUtils,
                     mAuthController, mTelephonyListenerManager,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index e8c760c..d1107c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -35,6 +35,8 @@
 import android.view.WindowManager
 import android.widget.ScrollView
 import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
+import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
 import com.android.internal.jank.InteractionJankMonitor
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.R
@@ -159,6 +161,35 @@
     }
 
     @Test
+    fun testDismissesOnFocusLoss_hidesKeyboardWhenVisible() {
+        val container = initializeFingerprintContainer(
+            authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL
+        )
+        waitForIdleSync()
+
+        val requestID = authContainer?.requestId ?: 0L
+
+        // Simulate keyboard was shown on the credential view
+        val windowInsetsController = container.windowInsetsController
+        spyOn(windowInsetsController)
+        spyOn(container.rootWindowInsets)
+        doReturn(true).`when`(container.rootWindowInsets).isVisible(WindowInsets.Type.ime())
+
+        container.onWindowFocusChanged(false)
+        waitForIdleSync()
+
+        // Expect hiding IME request will be invoked when dismissing the view
+        verify(windowInsetsController)?.hide(WindowInsets.Type.ime())
+
+        verify(callback).onDismissed(
+            eq(AuthDialogCallback.DISMISSED_USER_CANCELED),
+            eq<ByteArray?>(null), /* credentialAttestation */
+            eq(requestID)
+        )
+        assertThat(container.parent).isNull()
+    }
+
+    @Test
     fun testActionAuthenticated_sendsDismissedAuthenticated() {
         val container = initializeFingerprintContainer()
         container.mBiometricCallback.onAction(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
index a02dfa3..a275c8d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -87,6 +87,7 @@
 import com.android.systemui.keyguard.WakefulnessLifecycle;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.CommandQueue;
+import com.android.systemui.statusbar.VibratorHelper;
 import com.android.systemui.util.concurrency.DelayableExecutor;
 import com.android.systemui.util.concurrency.Execution;
 import com.android.systemui.util.concurrency.FakeExecution;
@@ -173,6 +174,9 @@
     private DelayableExecutor mBackgroundExecutor;
     private TestableAuthController mAuthController;
 
+    @Mock
+    private VibratorHelper mVibratorHelper;
+
     @Before
     public void setup() throws RemoteException {
         mContextSpy = spy(mContext);
@@ -221,7 +225,8 @@
 
         mAuthController = new TestableAuthController(mContextSpy, mExecution, mCommandQueue,
                 mActivityTaskManager, mWindowManager, mFingerprintManager, mFaceManager,
-                () -> mUdfpsController, () -> mSidefpsController, mStatusBarStateController);
+                () -> mUdfpsController, () -> mSidefpsController, mStatusBarStateController,
+                mVibratorHelper);
 
         mAuthController.start();
         verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(
@@ -246,11 +251,13 @@
         // This test is sensitive to prior FingerprintManager interactions.
         reset(mFingerprintManager);
 
+        when(mVibratorHelper.hasVibrator()).thenReturn(true);
+
         // This test requires an uninitialized AuthController.
         AuthController authController = new TestableAuthController(mContextSpy, mExecution,
                 mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager,
                 mFaceManager, () -> mUdfpsController, () -> mSidefpsController,
-                mStatusBarStateController);
+                mStatusBarStateController, mVibratorHelper);
         authController.start();
 
         verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(
@@ -270,11 +277,13 @@
         // This test is sensitive to prior FingerprintManager interactions.
         reset(mFingerprintManager);
 
+        when(mVibratorHelper.hasVibrator()).thenReturn(true);
+
         // This test requires an uninitialized AuthController.
         AuthController authController = new TestableAuthController(mContextSpy, mExecution,
                 mCommandQueue, mActivityTaskManager, mWindowManager, mFingerprintManager,
                 mFaceManager, () -> mUdfpsController, () -> mSidefpsController,
-                mStatusBarStateController);
+                mStatusBarStateController, mVibratorHelper);
         authController.start();
 
         verify(mFingerprintManager).addAuthenticatorsRegisteredCallback(
@@ -928,12 +937,13 @@
                 FaceManager faceManager,
                 Provider<UdfpsController> udfpsControllerFactory,
                 Provider<SidefpsController> sidefpsControllerFactory,
-                StatusBarStateController statusBarStateController) {
+                StatusBarStateController statusBarStateController,
+                VibratorHelper vibratorHelper) {
             super(context, execution, commandQueue, activityTaskManager, windowManager,
                     fingerprintManager, faceManager, udfpsControllerFactory,
                     sidefpsControllerFactory, mDisplayManager, mWakefulnessLifecycle,
                     mUserManager, mLockPatternUtils, statusBarStateController,
-                    mInteractionJankMonitor, mHandler, mBackgroundExecutor);
+                    mInteractionJankMonitor, mHandler, mBackgroundExecutor, vibratorHelper);
         }
 
         @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
index c85334d..90948ff 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerOverlayTest.kt
@@ -42,6 +42,8 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.ActivityLaunchAnimator
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.shade.ShadeExpansionStateManager
 import com.android.systemui.statusbar.LockscreenShadeTransitionController
@@ -103,6 +105,8 @@
     @Mock private lateinit var udfpsView: UdfpsView
     @Mock private lateinit var udfpsEnrollView: UdfpsEnrollView
     @Mock private lateinit var activityLaunchAnimator: ActivityLaunchAnimator
+    @Mock private lateinit var featureFlags: FeatureFlags
+    @Mock private lateinit var bouncerInteractor: BouncerInteractor
     @Captor private lateinit var layoutParamsCaptor: ArgumentCaptor<WindowManager.LayoutParams>
 
     private val onTouch = { _: View, _: MotionEvent, _: Boolean -> true }
@@ -136,7 +140,8 @@
             keyguardUpdateMonitor, dialogManager, dumpManager, transitionController,
             configurationController, systemClock, keyguardStateController,
             unlockedScreenOffAnimationController, udfpsDisplayMode, REQUEST_ID, reason,
-            controllerCallback, onTouch, activityLaunchAnimator, isDebuggable
+            controllerCallback, onTouch, activityLaunchAnimator, featureFlags,
+            bouncerInteractor, isDebuggable
         )
         block()
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 28e13b8..be39c0d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -69,7 +69,9 @@
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.animation.ActivityLaunchAnimator;
 import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FeatureFlags;
 import com.android.systemui.keyguard.ScreenLifecycle;
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor;
 import com.android.systemui.plugins.FalsingManager;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.shade.ShadeExpansionStateManager;
@@ -171,6 +173,8 @@
     private FakeExecutor mFgExecutor;
     @Mock
     private UdfpsDisplayMode mUdfpsDisplayMode;
+    @Mock
+    private FeatureFlags mFeatureFlags;
 
     // Stuff for configuring mocks
     @Mock
@@ -191,6 +195,8 @@
     private ActivityLaunchAnimator mActivityLaunchAnimator;
     @Mock
     private AlternateUdfpsTouchProvider mAlternateTouchProvider;
+    @Mock
+    private BouncerInteractor mBouncerInteractor;
 
     // Capture listeners so that they can be used to send events
     @Captor private ArgumentCaptor<IUdfpsOverlayController> mOverlayCaptor;
@@ -252,6 +258,7 @@
                 mStatusBarKeyguardViewManager,
                 mDumpManager,
                 mKeyguardUpdateMonitor,
+                mFeatureFlags,
                 mFalsingManager,
                 mPowerManager,
                 mAccessibilityManager,
@@ -270,7 +277,8 @@
                 mLatencyTracker,
                 mActivityLaunchAnimator,
                 Optional.of(mAlternateTouchProvider),
-                mBiometricsExecutor);
+                mBiometricsExecutor,
+                mBouncerInteractor);
         verify(mFingerprintManager).setUdfpsOverlayController(mOverlayCaptor.capture());
         mOverlayController = mOverlayCaptor.getValue();
         verify(mScreenLifecycle).addObserver(mScreenObserverCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
new file mode 100644
index 0000000..e5c7a42
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerBaseTest.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.animation.ActivityLaunchAnimator;
+import com.android.systemui.dump.DumpManager;
+import com.android.systemui.flags.FakeFeatureFlags;
+import com.android.systemui.flags.Flags;
+import com.android.systemui.keyguard.KeyguardViewMediator;
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor;
+import com.android.systemui.plugins.statusbar.StatusBarStateController;
+import com.android.systemui.shade.ShadeExpansionChangeEvent;
+import com.android.systemui.shade.ShadeExpansionListener;
+import com.android.systemui.shade.ShadeExpansionStateManager;
+import com.android.systemui.statusbar.LockscreenShadeTransitionController;
+import com.android.systemui.statusbar.phone.KeyguardBouncer;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
+import com.android.systemui.statusbar.phone.SystemUIDialogManager;
+import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.KeyguardStateController;
+import com.android.systemui.util.concurrency.DelayableExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+public class UdfpsKeyguardViewControllerBaseTest extends SysuiTestCase {
+    // Dependencies
+    protected @Mock UdfpsKeyguardView mView;
+    protected @Mock Context mResourceContext;
+    protected @Mock StatusBarStateController mStatusBarStateController;
+    protected @Mock ShadeExpansionStateManager mShadeExpansionStateManager;
+    protected @Mock StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
+    protected @Mock LockscreenShadeTransitionController mLockscreenShadeTransitionController;
+    protected @Mock DumpManager mDumpManager;
+    protected @Mock DelayableExecutor mExecutor;
+    protected @Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
+    protected @Mock KeyguardStateController mKeyguardStateController;
+    protected @Mock KeyguardViewMediator mKeyguardViewMediator;
+    protected @Mock ConfigurationController mConfigurationController;
+    protected @Mock UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
+    protected @Mock SystemUIDialogManager mDialogManager;
+    protected @Mock UdfpsController mUdfpsController;
+    protected @Mock ActivityLaunchAnimator mActivityLaunchAnimator;
+    protected @Mock KeyguardBouncer mBouncer;
+    protected @Mock BouncerInteractor mBouncerInteractor;
+
+    protected FakeFeatureFlags mFeatureFlags = new FakeFeatureFlags();
+    protected FakeSystemClock mSystemClock = new FakeSystemClock();
+
+    protected UdfpsKeyguardViewController mController;
+
+    // Capture listeners so that they can be used to send events
+    private @Captor ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor;
+    protected StatusBarStateController.StateListener mStatusBarStateListener;
+
+    private @Captor ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor;
+    protected List<ShadeExpansionListener> mExpansionListeners;
+
+    private @Captor ArgumentCaptor<StatusBarKeyguardViewManager.AlternateAuthInterceptor>
+            mAltAuthInterceptorCaptor;
+    protected StatusBarKeyguardViewManager.AlternateAuthInterceptor mAltAuthInterceptor;
+
+    private @Captor ArgumentCaptor<KeyguardStateController.Callback>
+            mKeyguardStateControllerCallbackCaptor;
+    protected KeyguardStateController.Callback mKeyguardStateControllerCallback;
+
+    @Before
+    public void setUp() {
+        MockitoAnnotations.initMocks(this);
+        when(mView.getContext()).thenReturn(mResourceContext);
+        when(mResourceContext.getString(anyInt())).thenReturn("test string");
+        when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(false);
+        when(mView.getUnpausedAlpha()).thenReturn(255);
+        mController = createUdfpsKeyguardViewController();
+    }
+
+    protected void sendStatusBarStateChanged(int statusBarState) {
+        mStatusBarStateListener.onStateChanged(statusBarState);
+    }
+
+    protected void captureStatusBarStateListeners() {
+        verify(mStatusBarStateController).addCallback(mStateListenerCaptor.capture());
+        mStatusBarStateListener = mStateListenerCaptor.getValue();
+    }
+
+    protected void captureStatusBarExpansionListeners() {
+        verify(mShadeExpansionStateManager, times(2))
+                .addExpansionListener(mExpansionListenerCaptor.capture());
+        // first (index=0) is from super class, UdfpsAnimationViewController.
+        // second (index=1) is from UdfpsKeyguardViewController
+        mExpansionListeners = mExpansionListenerCaptor.getAllValues();
+    }
+
+    protected void updateStatusBarExpansion(float fraction, boolean expanded) {
+        ShadeExpansionChangeEvent event =
+                new ShadeExpansionChangeEvent(
+                        fraction, expanded, /* tracking= */ false, /* dragDownPxAmount= */ 0f);
+        for (ShadeExpansionListener listener : mExpansionListeners) {
+            listener.onPanelExpansionChanged(event);
+        }
+    }
+
+    protected void captureAltAuthInterceptor() {
+        verify(mStatusBarKeyguardViewManager).setAlternateAuthInterceptor(
+                mAltAuthInterceptorCaptor.capture());
+        mAltAuthInterceptor = mAltAuthInterceptorCaptor.getValue();
+    }
+
+    protected void captureKeyguardStateControllerCallback() {
+        verify(mKeyguardStateController).addCallback(
+                mKeyguardStateControllerCallbackCaptor.capture());
+        mKeyguardStateControllerCallback = mKeyguardStateControllerCallbackCaptor.getValue();
+    }
+
+    public UdfpsKeyguardViewController createUdfpsKeyguardViewController() {
+        return createUdfpsKeyguardViewController(false);
+    }
+
+    protected UdfpsKeyguardViewController createUdfpsKeyguardViewController(
+            boolean useModernBouncer) {
+        mFeatureFlags.set(Flags.MODERN_BOUNCER, useModernBouncer);
+        when(mStatusBarKeyguardViewManager.getBouncer()).thenReturn(
+                useModernBouncer ? null : mBouncer);
+        return new UdfpsKeyguardViewController(
+                mView,
+                mStatusBarStateController,
+                mShadeExpansionStateManager,
+                mStatusBarKeyguardViewManager,
+                mKeyguardUpdateMonitor,
+                mDumpManager,
+                mLockscreenShadeTransitionController,
+                mConfigurationController,
+                mSystemClock,
+                mKeyguardStateController,
+                mUnlockedScreenOffAnimationController,
+                mDialogManager,
+                mUdfpsController,
+                mActivityLaunchAnimator,
+                mFeatureFlags,
+                mBouncerInteractor);
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
index c0f9c82..55b6194 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerTest.java
@@ -25,126 +25,53 @@
 import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
-import android.content.Context;
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper.RunWithLooper;
 
 import androidx.test.filters.SmallTest;
 
-import com.android.keyguard.KeyguardUpdateMonitor;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.animation.ActivityLaunchAnimator;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.keyguard.KeyguardViewMediator;
-import com.android.systemui.plugins.statusbar.StatusBarStateController;
-import com.android.systemui.shade.ShadeExpansionChangeEvent;
 import com.android.systemui.shade.ShadeExpansionListener;
-import com.android.systemui.shade.ShadeExpansionStateManager;
-import com.android.systemui.statusbar.LockscreenShadeTransitionController;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
-import com.android.systemui.statusbar.phone.SystemUIDialogManager;
-import com.android.systemui.statusbar.phone.UnlockedScreenOffAnimationController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-import com.android.systemui.statusbar.policy.KeyguardStateController;
-import com.android.systemui.util.concurrency.DelayableExecutor;
-import com.android.systemui.util.time.FakeSystemClock;
+import com.android.systemui.statusbar.phone.KeyguardBouncer;
 
-import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
 import org.mockito.Captor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-
-import java.util.List;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
-public class UdfpsKeyguardViewControllerTest extends SysuiTestCase {
-    // Dependencies
-    @Mock
-    private UdfpsKeyguardView mView;
-    @Mock
-    private Context mResourceContext;
-    @Mock
-    private StatusBarStateController mStatusBarStateController;
-    @Mock
-    private ShadeExpansionStateManager mShadeExpansionStateManager;
-    @Mock
-    private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
-    @Mock
-    private LockscreenShadeTransitionController mLockscreenShadeTransitionController;
-    @Mock
-    private DumpManager mDumpManager;
-    @Mock
-    private DelayableExecutor mExecutor;
-    @Mock
-    private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
-    @Mock
-    private KeyguardStateController mKeyguardStateController;
-    @Mock
-    private KeyguardViewMediator mKeyguardViewMediator;
-    @Mock
-    private ConfigurationController mConfigurationController;
-    @Mock
-    private UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController;
-    @Mock
-    private SystemUIDialogManager mDialogManager;
-    @Mock
-    private UdfpsController mUdfpsController;
-    @Mock
-    private ActivityLaunchAnimator mActivityLaunchAnimator;
-    private FakeSystemClock mSystemClock = new FakeSystemClock();
+public class UdfpsKeyguardViewControllerTest extends UdfpsKeyguardViewControllerBaseTest {
+    private @Captor ArgumentCaptor<KeyguardBouncer.BouncerExpansionCallback>
+            mBouncerExpansionCallbackCaptor;
+    private KeyguardBouncer.BouncerExpansionCallback mBouncerExpansionCallback;
 
-    private UdfpsKeyguardViewController mController;
-
-    // Capture listeners so that they can be used to send events
-    @Captor private ArgumentCaptor<StatusBarStateController.StateListener> mStateListenerCaptor;
-    private StatusBarStateController.StateListener mStatusBarStateListener;
-
-    @Captor private ArgumentCaptor<ShadeExpansionListener> mExpansionListenerCaptor;
-    private List<ShadeExpansionListener> mExpansionListeners;
-
-    @Captor private ArgumentCaptor<StatusBarKeyguardViewManager.AlternateAuthInterceptor>
-            mAltAuthInterceptorCaptor;
-    private StatusBarKeyguardViewManager.AlternateAuthInterceptor mAltAuthInterceptor;
-
-    @Captor private ArgumentCaptor<KeyguardStateController.Callback>
-            mKeyguardStateControllerCallbackCaptor;
-    private KeyguardStateController.Callback mKeyguardStateControllerCallback;
-
-    @Before
-    public void setUp() {
-        MockitoAnnotations.initMocks(this);
-        when(mView.getContext()).thenReturn(mResourceContext);
-        when(mResourceContext.getString(anyInt())).thenReturn("test string");
-        when(mKeyguardViewMediator.isAnimatingScreenOff()).thenReturn(false);
-        when(mView.getUnpausedAlpha()).thenReturn(255);
-        mController = new UdfpsKeyguardViewController(
-                mView,
-                mStatusBarStateController,
-                mShadeExpansionStateManager,
-                mStatusBarKeyguardViewManager,
-                mKeyguardUpdateMonitor,
-                mDumpManager,
-                mLockscreenShadeTransitionController,
-                mConfigurationController,
-                mSystemClock,
-                mKeyguardStateController,
-                mUnlockedScreenOffAnimationController,
-                mDialogManager,
-                mUdfpsController,
-                mActivityLaunchAnimator);
+    @Override
+    public UdfpsKeyguardViewController createUdfpsKeyguardViewController() {
+        return createUdfpsKeyguardViewController(/* useModernBouncer */ false);
     }
 
     @Test
+    public void testShouldPauseAuth_bouncerShowing() {
+        mController.onViewAttached();
+        captureStatusBarStateListeners();
+        sendStatusBarStateChanged(StatusBarState.KEYGUARD);
+
+        captureBouncerExpansionCallback();
+        when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()).thenReturn(true);
+        mBouncerExpansionCallback.onVisibilityChanged(true);
+
+        assertTrue(mController.shouldPauseAuth());
+    }
+
+
+
+    @Test
     public void testRegistersExpansionChangedListenerOnAttached() {
         mController.onViewAttached();
         captureStatusBarExpansionListeners();
@@ -202,20 +129,6 @@
     }
 
     @Test
-    public void testShouldPauseAuthBouncerShowing() {
-        mController.onViewAttached();
-        captureStatusBarStateListeners();
-        sendStatusBarStateChanged(StatusBarState.KEYGUARD);
-
-        captureAltAuthInterceptor();
-        when(mStatusBarKeyguardViewManager.isBouncerShowing()).thenReturn(true);
-        when(mStatusBarKeyguardViewManager.bouncerIsOrWillBeShowing()).thenReturn(true);
-        mAltAuthInterceptor.onBouncerVisibilityChanged();
-
-        assertTrue(mController.shouldPauseAuth());
-    }
-
-    @Test
     public void testShouldPauseAuthUnpausedAlpha0() {
         mController.onViewAttached();
         captureStatusBarStateListeners();
@@ -503,41 +416,8 @@
         verify(mView, atLeastOnce()).setPauseAuth(false);
     }
 
-    private void sendStatusBarStateChanged(int statusBarState) {
-        mStatusBarStateListener.onStateChanged(statusBarState);
-    }
-
-    private void captureStatusBarStateListeners() {
-        verify(mStatusBarStateController).addCallback(mStateListenerCaptor.capture());
-        mStatusBarStateListener = mStateListenerCaptor.getValue();
-    }
-
-    private void captureStatusBarExpansionListeners() {
-        verify(mShadeExpansionStateManager, times(2))
-                .addExpansionListener(mExpansionListenerCaptor.capture());
-        // first (index=0) is from super class, UdfpsAnimationViewController.
-        // second (index=1) is from UdfpsKeyguardViewController
-        mExpansionListeners = mExpansionListenerCaptor.getAllValues();
-    }
-
-    private void updateStatusBarExpansion(float fraction, boolean expanded) {
-        ShadeExpansionChangeEvent event =
-                new ShadeExpansionChangeEvent(
-                        fraction, expanded, /* tracking= */ false, /* dragDownPxAmount= */ 0f);
-        for (ShadeExpansionListener listener : mExpansionListeners) {
-            listener.onPanelExpansionChanged(event);
-        }
-    }
-
-    private void captureAltAuthInterceptor() {
-        verify(mStatusBarKeyguardViewManager).setAlternateAuthInterceptor(
-                mAltAuthInterceptorCaptor.capture());
-        mAltAuthInterceptor = mAltAuthInterceptorCaptor.getValue();
-    }
-
-    private void captureKeyguardStateControllerCallback() {
-        verify(mKeyguardStateController).addCallback(
-                mKeyguardStateControllerCallbackCaptor.capture());
-        mKeyguardStateControllerCallback = mKeyguardStateControllerCallbackCaptor.getValue();
+    private void captureBouncerExpansionCallback() {
+        verify(mBouncer).addBouncerExpansionCallback(mBouncerExpansionCallbackCaptor.capture());
+        mBouncerExpansionCallback = mBouncerExpansionCallbackCaptor.getValue();
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.kt
new file mode 100644
index 0000000..7b19768
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsKeyguardViewControllerWithCoroutinesTest.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.biometrics
+
+import android.os.Handler
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.keyguard.KeyguardSecurityModel
+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.domain.interactor.BouncerCallbackInteractor
+import com.android.systemui.keyguard.domain.interactor.BouncerInteractor
+import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.phone.KeyguardBouncer
+import com.android.systemui.statusbar.phone.KeyguardBypassController
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.yield
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.mock
+import org.mockito.MockitoAnnotations
+
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+@TestableLooper.RunWithLooper
+class UdfpsKeyguardViewControllerWithCoroutinesTest : UdfpsKeyguardViewControllerBaseTest() {
+    lateinit var keyguardBouncerRepository: KeyguardBouncerRepository
+
+    @Before
+    override fun setUp() {
+        allowTestableLooperAsMainThread() // repeatWhenAttached requires the main thread
+        MockitoAnnotations.initMocks(this)
+        keyguardBouncerRepository =
+            KeyguardBouncerRepository(
+                mock(com.android.keyguard.ViewMediatorCallback::class.java),
+                mKeyguardUpdateMonitor
+            )
+        super.setUp()
+    }
+
+    override fun createUdfpsKeyguardViewController(): UdfpsKeyguardViewController? {
+        mBouncerInteractor =
+            BouncerInteractor(
+                keyguardBouncerRepository,
+                mock(BouncerView::class.java),
+                mock(Handler::class.java),
+                mKeyguardStateController,
+                mock(KeyguardSecurityModel::class.java),
+                mock(BouncerCallbackInteractor::class.java),
+                mock(FalsingCollector::class.java),
+                mock(DismissCallbackRegistry::class.java),
+                mock(KeyguardBypassController::class.java),
+                mKeyguardUpdateMonitor
+            )
+        return createUdfpsKeyguardViewController(/* useModernBouncer */ true)
+    }
+
+    /** After migration, replaces LockIconViewControllerTest version */
+    @Test
+    fun testShouldPauseAuthBouncerShowing() =
+        runBlocking(IMMEDIATE) {
+            // GIVEN view attached and we're on the keyguard
+            mController.onViewAttached()
+            captureStatusBarStateListeners()
+            sendStatusBarStateChanged(StatusBarState.KEYGUARD)
+
+            // WHEN the bouncer expansion is VISIBLE
+            val job = mController.listenForBouncerExpansion(this)
+            keyguardBouncerRepository.setVisible(true)
+            keyguardBouncerRepository.setPanelExpansion(KeyguardBouncer.EXPANSION_VISIBLE)
+            yield()
+
+            // THEN UDFPS shouldPauseAuth == true
+            assertTrue(mController.shouldPauseAuth())
+
+            job.cancel()
+        }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
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 c5a7de4..c234178 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -92,6 +92,12 @@
     @Mock
     BouncerCallbackInteractor mBouncerCallbackInteractor;
 
+    @Mock
+    DreamOverlayAnimationsController mAnimationsController;
+
+    @Mock
+    DreamOverlayStateController mStateController;
+
     DreamOverlayContainerViewController mController;
 
     @Before
@@ -115,7 +121,9 @@
                 MAX_BURN_IN_OFFSET,
                 BURN_IN_PROTECTION_UPDATE_INTERVAL,
                 MILLIS_UNTIL_FULL_JITTER,
-                mBouncerCallbackInteractor);
+                mBouncerCallbackInteractor,
+                mAnimationsController,
+                mStateController);
     }
 
     @Test
@@ -188,4 +196,31 @@
         verify(mBlurUtils).blurRadiusOfRatio(1 - scaledFraction);
         verify(mBlurUtils).applyBlur(mViewRoot, (int) blurRadius, false);
     }
+
+    @Test
+    public void testStartDreamEntryAnimationsOnAttachedNonLowLight() {
+        when(mStateController.isLowLightActive()).thenReturn(false);
+
+        mController.onViewAttached();
+
+        verify(mAnimationsController).startEntryAnimations(mDreamOverlayContainerView);
+        verify(mAnimationsController, never()).cancelRunningEntryAnimations();
+    }
+
+    @Test
+    public void testNeverStartDreamEntryAnimationsOnAttachedForLowLight() {
+        when(mStateController.isLowLightActive()).thenReturn(true);
+
+        mController.onViewAttached();
+
+        verify(mAnimationsController, never()).startEntryAnimations(mDreamOverlayContainerView);
+    }
+
+    @Test
+    public void testCancelDreamEntryAnimationsOnDetached() {
+        mController.onViewAttached();
+        mController.onViewDetached();
+
+        verify(mAnimationsController).cancelRunningEntryAnimations();
+    }
 }
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 f370be1..b7f694f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayServiceTest.java
@@ -253,6 +253,7 @@
         verify(mLifecycleRegistry).setCurrentState(Lifecycle.State.DESTROYED);
         verify(mStateController).setOverlayActive(false);
         verify(mStateController).setLowLightActive(false);
+        verify(mStateController).setEntryAnimationsFinished(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 d1d32a1..c21c7a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java
@@ -234,4 +234,20 @@
         verify(mCallback, times(1)).onStateChanged();
         assertThat(stateController.isLowLightActive()).isTrue();
     }
+
+    @Test
+    public void testNotifyEntryAnimationsFinishedChanged() {
+        final DreamOverlayStateController stateController =
+                new DreamOverlayStateController(mExecutor);
+
+        stateController.addCallback(mCallback);
+        mExecutor.runAllReady();
+        assertThat(stateController.areEntryAnimationsFinished()).isFalse();
+
+        stateController.setEntryAnimationsFinished(true);
+        mExecutor.runAllReady();
+
+        verify(mCallback, times(1)).onStateChanged();
+        assertThat(stateController.areEntryAnimationsFinished()).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 aa02178..85c2819 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/DreamOverlayStatusBarViewControllerTest.java
@@ -19,16 +19,20 @@
 import static android.app.StatusBarManager.WINDOW_STATE_HIDDEN;
 import static android.app.StatusBarManager.WINDOW_STATE_SHOWING;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doCallRealMethod;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.reset;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.AlarmManager;
+import android.content.Context;
 import android.content.res.Resources;
 import android.hardware.SensorPrivacyManager;
 import android.net.ConnectivityManager;
@@ -55,6 +59,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
@@ -69,7 +74,7 @@
             "{count, plural, =1 {# notification} other {# notifications}}";
 
     @Mock
-    DreamOverlayStatusBarView mView;
+    MockDreamOverlayStatusBarView mView;
     @Mock
     ConnectivityManager mConnectivityManager;
     @Mock
@@ -105,6 +110,9 @@
     @Mock
     DreamOverlayStateController mDreamOverlayStateController;
 
+    @Captor
+    private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor;
+
     private final Executor mMainExecutor = Runnable::run;
 
     DreamOverlayStatusBarViewController mController;
@@ -115,6 +123,8 @@
 
         when(mResources.getString(R.string.dream_overlay_status_bar_notification_indicator))
                 .thenReturn(NOTIFICATION_INDICATOR_FORMATTER_STRING);
+        doCallRealMethod().when(mView).setVisibility(anyInt());
+        doCallRealMethod().when(mView).getVisibility();
 
         mController = new DreamOverlayStatusBarViewController(
                 mView,
@@ -454,12 +464,10 @@
     public void testStatusBarHiddenWhenSystemStatusBarShown() {
         mController.onViewAttached();
 
-        final ArgumentCaptor<StatusBarWindowStateListener>
-                callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
-        verify(mStatusBarWindowStateController).addListener(callbackCapture.capture());
-        callbackCapture.getValue().onStatusBarWindowStateChanged(WINDOW_STATE_SHOWING);
+        updateEntryAnimationsFinished();
+        updateStatusBarWindowState(true);
 
-        verify(mView).setVisibility(View.INVISIBLE);
+        assertThat(mView.getVisibility()).isEqualTo(View.INVISIBLE);
     }
 
     @Test
@@ -467,29 +475,43 @@
         mController.onViewAttached();
         reset(mView);
 
-        final ArgumentCaptor<StatusBarWindowStateListener>
-                callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
-        verify(mStatusBarWindowStateController).addListener(callbackCapture.capture());
-        callbackCapture.getValue().onStatusBarWindowStateChanged(WINDOW_STATE_HIDDEN);
+        updateEntryAnimationsFinished();
+        updateStatusBarWindowState(false);
 
-        verify(mView).setVisibility(View.VISIBLE);
+        assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
     }
 
     @Test
     public void testUnattachedStatusBarVisibilityUnchangedWhenSystemStatusBarHidden() {
         mController.onViewAttached();
+        updateEntryAnimationsFinished();
         mController.onViewDetached();
         reset(mView);
 
-        final ArgumentCaptor<StatusBarWindowStateListener>
-                callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
-        verify(mStatusBarWindowStateController).addListener(callbackCapture.capture());
-        callbackCapture.getValue().onStatusBarWindowStateChanged(WINDOW_STATE_SHOWING);
+        updateStatusBarWindowState(true);
 
         verify(mView, never()).setVisibility(anyInt());
     }
 
     @Test
+    public void testNoChangeToVisibilityBeforeDreamStartedWhenStatusBarHidden() {
+        mController.onViewAttached();
+
+        // Trigger status bar window state change.
+        final StatusBarWindowStateListener listener = updateStatusBarWindowState(false);
+
+        // Verify no visibility change because dream not started.
+        verify(mView, never()).setVisibility(anyInt());
+
+        // Dream entry animations finished.
+        updateEntryAnimationsFinished();
+
+        // Trigger another status bar window state change, and verify visibility change.
+        listener.onStatusBarWindowStateChanged(WINDOW_STATE_HIDDEN);
+        assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
     public void testExtraStatusBarItemSetWhenItemsChange() {
         mController.onViewAttached();
         when(mStatusBarItem.getView()).thenReturn(mStatusBarItemView);
@@ -507,16 +529,75 @@
     public void testLowLightHidesStatusBar() {
         when(mDreamOverlayStateController.isLowLightActive()).thenReturn(true);
         mController.onViewAttached();
+        updateEntryAnimationsFinished();
 
-        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);
+        assertThat(mView.getVisibility()).isEqualTo(View.INVISIBLE);
+        reset(mView);
+
+        when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false);
+        callbackCapture.getValue().onStateChanged();
+
+        assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    @Test
+    public void testNoChangeToVisibilityBeforeDreamStartedWhenLowLightStateChange() {
+        when(mDreamOverlayStateController.isLowLightActive()).thenReturn(false);
+        mController.onViewAttached();
+
+        // No change to visibility because dream not fully started.
+        verify(mView, never()).setVisibility(anyInt());
+
+        // Dream entry animations finished.
+        updateEntryAnimationsFinished();
+
+        // Trigger state change and verify visibility changed.
+        final ArgumentCaptor<DreamOverlayStateController.Callback> callbackCapture =
+                ArgumentCaptor.forClass(DreamOverlayStateController.Callback.class);
+        verify(mDreamOverlayStateController).addCallback(callbackCapture.capture());
+        callbackCapture.getValue().onStateChanged();
+
+        assertThat(mView.getVisibility()).isEqualTo(View.VISIBLE);
+    }
+
+    private StatusBarWindowStateListener updateStatusBarWindowState(boolean show) {
+        when(mStatusBarWindowStateController.windowIsShowing()).thenReturn(show);
+        final ArgumentCaptor<StatusBarWindowStateListener>
+                callbackCapture = ArgumentCaptor.forClass(StatusBarWindowStateListener.class);
+        verify(mStatusBarWindowStateController).addListener(callbackCapture.capture());
+        final StatusBarWindowStateListener listener = callbackCapture.getValue();
+        listener.onStatusBarWindowStateChanged(show ? WINDOW_STATE_SHOWING : WINDOW_STATE_HIDDEN);
+        return listener;
+    }
+
+    private void updateEntryAnimationsFinished() {
+        when(mDreamOverlayStateController.areEntryAnimationsFinished()).thenReturn(true);
+
+        verify(mDreamOverlayStateController).addCallback(mCallbackCaptor.capture());
+        final DreamOverlayStateController.Callback callback = mCallbackCaptor.getValue();
+        callback.onStateChanged();
+    }
+
+    private static class MockDreamOverlayStatusBarView extends DreamOverlayStatusBarView {
+        private int mVisibility = View.VISIBLE;
+
+        private MockDreamOverlayStatusBarView(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void setVisibility(int visibility) {
+            mVisibility = visibility;
+        }
+
+        @Override
+        public int getVisibility() {
+            return mVisibility;
+        }
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
index 3b9e398..b477592 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/dreams/complication/ComplicationHostViewControllerTest.java
@@ -16,6 +16,7 @@
 package com.android.systemui.dreams.complication;
 
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -29,16 +30,18 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.dreams.DreamOverlayStateController;
 
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 
 @SmallTest
@@ -77,9 +80,20 @@
     @Mock
     ComplicationLayoutParams mComplicationLayoutParams;
 
+    @Mock
+    DreamOverlayStateController mDreamOverlayStateController;
+
+    @Captor
+    private ArgumentCaptor<Observer<Collection<ComplicationViewModel>>> mObserverCaptor;
+
+    @Captor
+    private ArgumentCaptor<DreamOverlayStateController.Callback> mCallbackCaptor;
+
     @Complication.Category
     static final int COMPLICATION_CATEGORY = Complication.CATEGORY_SYSTEM;
 
+    private ComplicationHostViewController mController;
+
     @Before
     public void setup() {
         MockitoAnnotations.initMocks(this);
@@ -91,6 +105,15 @@
         when(mViewHolder.getCategory()).thenReturn(COMPLICATION_CATEGORY);
         when(mViewHolder.getLayoutParams()).thenReturn(mComplicationLayoutParams);
         when(mComplicationView.getParent()).thenReturn(mComplicationHostView);
+
+        mController = new ComplicationHostViewController(
+                mComplicationHostView,
+                mLayoutEngine,
+                mDreamOverlayStateController,
+                mLifecycleOwner,
+                mViewModel);
+
+        mController.init();
     }
 
     /**
@@ -98,25 +121,12 @@
      */
     @Test
     public void testViewModelObservation() {
-        final ArgumentCaptor<Observer<Collection<ComplicationViewModel>>> observerArgumentCaptor =
-                ArgumentCaptor.forClass(Observer.class);
-        final ComplicationHostViewController controller = new ComplicationHostViewController(
-                mComplicationHostView,
-                mLayoutEngine,
-                mLifecycleOwner,
-                mViewModel);
-
-        controller.init();
-
-        verify(mComplicationViewModelLiveData).observe(eq(mLifecycleOwner),
-                observerArgumentCaptor.capture());
-
         final Observer<Collection<ComplicationViewModel>> observer =
-                observerArgumentCaptor.getValue();
+                captureComplicationViewModelsObserver();
 
-        // Add complication and ensure it is added to the view.
+        // Add a complication and ensure it is added to the view.
         final HashSet<ComplicationViewModel> complications = new HashSet<>(
-                Arrays.asList(mComplicationViewModel));
+                Collections.singletonList(mComplicationViewModel));
         observer.onChanged(complications);
 
         verify(mLayoutEngine).addComplication(eq(mComplicationId), eq(mComplicationView),
@@ -127,4 +137,48 @@
 
         verify(mLayoutEngine).removeComplication(eq(mComplicationId));
     }
+
+    @Test
+    public void testNewComplicationsBeforeEntryAnimationsFinishSetToInvisible() {
+        final Observer<Collection<ComplicationViewModel>> observer =
+                captureComplicationViewModelsObserver();
+
+        // Add a complication before entry animations are finished.
+        final HashSet<ComplicationViewModel> complications = new HashSet<>(
+                Collections.singletonList(mComplicationViewModel));
+        observer.onChanged(complications);
+
+        // The complication view should be set to invisible.
+        verify(mComplicationView).setVisibility(View.INVISIBLE);
+    }
+
+    @Test
+    public void testNewComplicationsAfterEntryAnimationsFinishNotSetToInvisible() {
+        final Observer<Collection<ComplicationViewModel>> observer =
+                captureComplicationViewModelsObserver();
+
+        // Dream entry animations finished.
+        when(mDreamOverlayStateController.areEntryAnimationsFinished()).thenReturn(true);
+        final DreamOverlayStateController.Callback stateCallback = captureOverlayStateCallback();
+        stateCallback.onStateChanged();
+
+        // Add a complication after entry animations are finished.
+        final HashSet<ComplicationViewModel> complications = new HashSet<>(
+                Collections.singletonList(mComplicationViewModel));
+        observer.onChanged(complications);
+
+        // The complication view should not be set to invisible.
+        verify(mComplicationView, never()).setVisibility(View.INVISIBLE);
+    }
+
+    private Observer<Collection<ComplicationViewModel>> captureComplicationViewModelsObserver() {
+        verify(mComplicationViewModelLiveData).observe(eq(mLifecycleOwner),
+                mObserverCaptor.capture());
+        return mObserverCaptor.getValue();
+    }
+
+    private DreamOverlayStateController.Callback captureOverlayStateCallback() {
+        verify(mDreamOverlayStateController).addCallback(mCallbackCaptor.capture());
+        return mCallbackCaptor.getValue();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
index f18acba..0fb181d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt
@@ -23,14 +23,11 @@
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.yield
 
-/**
- * Fake implementation of a quick affordance data source.
- *
- * This class is abstract to force tests to provide extensions of it as the system that references
- * these configs uses each implementation's class type to refer to them.
- */
-abstract class FakeKeyguardQuickAffordanceConfig(
+/** Fake implementation of a quick affordance data source. */
+class FakeKeyguardQuickAffordanceConfig(
     override val key: String,
+    override val pickerName: String = key,
+    override val pickerIconResourceId: Int = 0,
 ) : KeyguardQuickAffordanceConfig {
 
     var onTriggeredResult: OnTriggeredResult = OnTriggeredResult.Handled
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt
new file mode 100644
index 0000000..d2422ad
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceSelectionManagerTest.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.quickaffordance
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordanceSelectionManagerTest : SysuiTestCase() {
+
+    private lateinit var underTest: KeyguardQuickAffordanceSelectionManager
+
+    @Before
+    fun setUp() {
+        underTest = KeyguardQuickAffordanceSelectionManager()
+    }
+
+    @Test
+    fun setSelections() =
+        runBlocking(IMMEDIATE) {
+            var affordanceIdsBySlotId: Map<String, List<String>>? = null
+            val job = underTest.selections.onEach { affordanceIdsBySlotId = it }.launchIn(this)
+            val slotId1 = "slot1"
+            val slotId2 = "slot2"
+            val affordanceId1 = "affordance1"
+            val affordanceId2 = "affordance2"
+            val affordanceId3 = "affordance3"
+
+            underTest.setSelections(
+                slotId = slotId1,
+                affordanceIds = listOf(affordanceId1),
+            )
+            assertSelections(
+                affordanceIdsBySlotId,
+                mapOf(
+                    slotId1 to listOf(affordanceId1),
+                ),
+            )
+
+            underTest.setSelections(
+                slotId = slotId2,
+                affordanceIds = listOf(affordanceId2),
+            )
+            assertSelections(
+                affordanceIdsBySlotId,
+                mapOf(
+                    slotId1 to listOf(affordanceId1),
+                    slotId2 to listOf(affordanceId2),
+                )
+            )
+
+            underTest.setSelections(
+                slotId = slotId1,
+                affordanceIds = listOf(affordanceId1, affordanceId3),
+            )
+            assertSelections(
+                affordanceIdsBySlotId,
+                mapOf(
+                    slotId1 to listOf(affordanceId1, affordanceId3),
+                    slotId2 to listOf(affordanceId2),
+                )
+            )
+
+            underTest.setSelections(
+                slotId = slotId1,
+                affordanceIds = listOf(affordanceId3),
+            )
+            assertSelections(
+                affordanceIdsBySlotId,
+                mapOf(
+                    slotId1 to listOf(affordanceId3),
+                    slotId2 to listOf(affordanceId2),
+                )
+            )
+
+            underTest.setSelections(
+                slotId = slotId2,
+                affordanceIds = listOf(),
+            )
+            assertSelections(
+                affordanceIdsBySlotId,
+                mapOf(
+                    slotId1 to listOf(affordanceId3),
+                    slotId2 to listOf(),
+                )
+            )
+
+            job.cancel()
+        }
+
+    private suspend fun assertSelections(
+        observed: Map<String, List<String>>?,
+        expected: Map<String, List<String>>,
+    ) {
+        assertThat(underTest.getSelections()).isEqualTo(expected)
+        assertThat(observed).isEqualTo(expected)
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
index 61a3f9f..2bd8e9a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt
@@ -50,7 +50,7 @@
         MockitoAnnotations.initMocks(this)
         whenever(controller.intent).thenReturn(INTENT_1)
 
-        underTest = QrCodeScannerKeyguardQuickAffordanceConfig(controller)
+        underTest = QrCodeScannerKeyguardQuickAffordanceConfig(mock(), controller)
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
index c05beef..5178154 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt
@@ -59,6 +59,7 @@
 
         underTest =
             QuickAccessWalletKeyguardQuickAffordanceConfig(
+                mock(),
                 walletController,
                 activityStarter,
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
new file mode 100644
index 0000000..5a7f2bb
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package com.android.systemui.keyguard.data.repository
+
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
+import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerRepresentation
+import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(JUnit4::class)
+class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
+
+    private lateinit var underTest: KeyguardQuickAffordanceRepository
+
+    private lateinit var config1: FakeKeyguardQuickAffordanceConfig
+    private lateinit var config2: FakeKeyguardQuickAffordanceConfig
+
+    @Before
+    fun setUp() {
+        config1 = FakeKeyguardQuickAffordanceConfig("built_in:1")
+        config2 = FakeKeyguardQuickAffordanceConfig("built_in:2")
+        underTest =
+            KeyguardQuickAffordanceRepository(
+                scope = CoroutineScope(IMMEDIATE),
+                backgroundDispatcher = IMMEDIATE,
+                selectionManager = KeyguardQuickAffordanceSelectionManager(),
+                configs = setOf(config1, config2),
+            )
+    }
+
+    @Test
+    fun setSelections() =
+        runBlocking(IMMEDIATE) {
+            var configsBySlotId: Map<String, List<KeyguardQuickAffordanceConfig>>? = null
+            val job = underTest.selections.onEach { configsBySlotId = it }.launchIn(this)
+            val slotId1 = "slot1"
+            val slotId2 = "slot2"
+
+            underTest.setSelections(slotId1, listOf(config1.key))
+            assertSelections(
+                configsBySlotId,
+                mapOf(
+                    slotId1 to listOf(config1),
+                ),
+            )
+
+            underTest.setSelections(slotId2, listOf(config2.key))
+            assertSelections(
+                configsBySlotId,
+                mapOf(
+                    slotId1 to listOf(config1),
+                    slotId2 to listOf(config2),
+                ),
+            )
+
+            underTest.setSelections(slotId1, emptyList())
+            underTest.setSelections(slotId2, listOf(config1.key))
+            assertSelections(
+                configsBySlotId,
+                mapOf(
+                    slotId1 to emptyList(),
+                    slotId2 to listOf(config1),
+                ),
+            )
+
+            job.cancel()
+        }
+
+    @Test
+    fun getAffordancePickerRepresentations() {
+        assertThat(underTest.getAffordancePickerRepresentations())
+            .isEqualTo(
+                listOf(
+                    KeyguardQuickAffordancePickerRepresentation(
+                        id = config1.key,
+                        name = config1.pickerName,
+                        iconResourceId = config1.pickerIconResourceId,
+                    ),
+                    KeyguardQuickAffordancePickerRepresentation(
+                        id = config2.key,
+                        name = config2.pickerName,
+                        iconResourceId = config2.pickerIconResourceId,
+                    ),
+                )
+            )
+    }
+
+    @Test
+    fun getSlotPickerRepresentations() {
+        assertThat(underTest.getSlotPickerRepresentations())
+            .isEqualTo(
+                listOf(
+                    KeyguardSlotPickerRepresentation(
+                        id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                        maxSelectedAffordances = 1,
+                    ),
+                    KeyguardSlotPickerRepresentation(
+                        id = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                        maxSelectedAffordances = 1,
+                    ),
+                )
+            )
+    }
+
+    private suspend fun assertSelections(
+        observed: Map<String, List<KeyguardQuickAffordanceConfig>>?,
+        expected: Map<String, List<KeyguardQuickAffordanceConfig>>,
+    ) {
+        assertThat(observed).isEqualTo(expected)
+        assertThat(underTest.getSelections())
+            .isEqualTo(expected.mapValues { (_, configs) -> configs.map { it.key } })
+        expected.forEach { (slotId, configs) ->
+            assertThat(underTest.getSelections(slotId)).isEqualTo(configs)
+        }
+    }
+
+    companion object {
+        private val IMMEDIATE = Dispatchers.Main.immediate
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
index b9ab9d3..53d9b87 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt
@@ -21,8 +21,10 @@
 import com.android.systemui.common.shared.model.Position
 import com.android.systemui.doze.DozeHost
 import com.android.systemui.keyguard.WakefulnessLifecycle
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.plugins.statusbar.StatusBarStateController
+import com.android.systemui.statusbar.phone.BiometricUnlockController
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.whenever
@@ -46,6 +48,7 @@
     @Mock private lateinit var dozeHost: DozeHost
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle
+    @Mock private lateinit var biometricUnlockController: BiometricUnlockController
 
     private lateinit var underTest: KeyguardRepositoryImpl
 
@@ -59,6 +62,7 @@
                 keyguardStateController,
                 dozeHost,
                 wakefulnessLifecycle,
+                biometricUnlockController,
             )
     }
 
@@ -190,7 +194,7 @@
     }
 
     @Test
-    fun wakefullness() = runBlockingTest {
+    fun wakefulness() = runBlockingTest {
         val values = mutableListOf<WakefulnessModel>()
         val job = underTest.wakefulnessState.onEach(values::add).launchIn(this)
 
@@ -217,4 +221,63 @@
         job.cancel()
         verify(wakefulnessLifecycle).removeObserver(captor.value)
     }
+
+    @Test
+    fun isBouncerShowing() = runBlockingTest {
+        whenever(keyguardStateController.isBouncerShowing).thenReturn(false)
+        var latest: Boolean? = null
+        val job = underTest.isBouncerShowing.onEach { latest = it }.launchIn(this)
+
+        assertThat(latest).isFalse()
+
+        val captor = argumentCaptor<KeyguardStateController.Callback>()
+        verify(keyguardStateController).addCallback(captor.capture())
+
+        whenever(keyguardStateController.isBouncerShowing).thenReturn(true)
+        captor.value.onBouncerShowingChanged()
+        assertThat(latest).isTrue()
+
+        whenever(keyguardStateController.isBouncerShowing).thenReturn(false)
+        captor.value.onBouncerShowingChanged()
+        assertThat(latest).isFalse()
+
+        job.cancel()
+    }
+
+    @Test
+    fun biometricUnlockState() = runBlockingTest {
+        val values = mutableListOf<BiometricUnlockModel>()
+        val job = underTest.biometricUnlockState.onEach(values::add).launchIn(this)
+
+        val captor = argumentCaptor<BiometricUnlockController.BiometricModeListener>()
+        verify(biometricUnlockController).addBiometricModeListener(captor.capture())
+
+        captor.value.onModeChanged(BiometricUnlockController.MODE_NONE)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_SHOW_BOUNCER)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_ONLY_WAKE)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_UNLOCK_COLLAPSING)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_DISMISS_BOUNCER)
+        captor.value.onModeChanged(BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM)
+
+        assertThat(values)
+            .isEqualTo(
+                listOf(
+                    // Initial value will be NONE, followed by onModeChanged() call
+                    BiometricUnlockModel.NONE,
+                    BiometricUnlockModel.NONE,
+                    BiometricUnlockModel.WAKE_AND_UNLOCK,
+                    BiometricUnlockModel.WAKE_AND_UNLOCK_PULSING,
+                    BiometricUnlockModel.SHOW_BOUNCER,
+                    BiometricUnlockModel.ONLY_WAKE,
+                    BiometricUnlockModel.UNLOCK_COLLAPSING,
+                    BiometricUnlockModel.DISMISS_BOUNCER,
+                    BiometricUnlockModel.WAKE_AND_UNLOCK_FROM_DREAM,
+                )
+            )
+
+        job.cancel()
+        verify(biometricUnlockController).removeBiometricModeListener(captor.value)
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index 64913c7c..27d5d0a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -128,11 +128,15 @@
 
             assertThat(steps.size).isEqualTo(3)
             assertThat(steps[0])
-                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED))
+                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME))
             assertThat(steps[1])
-                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING))
+                .isEqualTo(
+                    TransitionStep(AOD, LOCKSCREEN, 0.5f, TransitionState.RUNNING, OWNER_NAME)
+                )
             assertThat(steps[2])
-                .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED))
+                .isEqualTo(
+                    TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME)
+                )
             job.cancel()
         }
 
@@ -174,15 +178,22 @@
     }
 
     private fun assertSteps(steps: List<TransitionStep>, fractions: List<BigDecimal>) {
-        assertThat(steps[0]).isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED))
+        assertThat(steps[0])
+            .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME))
         fractions.forEachIndexed { index, fraction ->
             assertThat(steps[index + 1])
                 .isEqualTo(
-                    TransitionStep(AOD, LOCKSCREEN, fraction.toFloat(), TransitionState.RUNNING)
+                    TransitionStep(
+                        AOD,
+                        LOCKSCREEN,
+                        fraction.toFloat(),
+                        TransitionState.RUNNING,
+                        OWNER_NAME
+                    )
                 )
         }
         assertThat(steps[steps.size - 1])
-            .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED))
+            .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME))
 
         assertThat(wtfHandler.failed).isFalse()
     }
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
index e6c8dd8..5743b2f 100644
--- 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
@@ -27,8 +27,8 @@
 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.BouncerViewDelegate
 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
@@ -57,6 +57,7 @@
     @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 bouncerViewDelegate: BouncerViewDelegate
     @Mock private lateinit var keyguardStateController: KeyguardStateController
     @Mock private lateinit var keyguardSecurityModel: KeyguardSecurityModel
     @Mock private lateinit var bouncerCallbackInteractor: BouncerCallbackInteractor
@@ -86,6 +87,7 @@
             )
         `when`(repository.startingDisappearAnimation.value).thenReturn(null)
         `when`(repository.show.value).thenReturn(null)
+        `when`(bouncerView.delegate).thenReturn(bouncerViewDelegate)
     }
 
     @Test
@@ -97,7 +99,7 @@
         verify(repository).setHide(false)
         verify(repository).setStartingToHide(false)
         verify(repository).setScrimmed(true)
-        verify(repository).setExpansion(EXPANSION_VISIBLE)
+        verify(repository).setPanelExpansion(EXPANSION_VISIBLE)
         verify(repository).setShowingSoon(true)
         verify(keyguardStateController).notifyBouncerShowing(true)
         verify(bouncerCallbackInteractor).dispatchStartingToShow()
@@ -108,7 +110,7 @@
 
     @Test
     fun testShow_isNotScrimmed() {
-        verify(repository, never()).setExpansion(EXPANSION_VISIBLE)
+        verify(repository, never()).setPanelExpansion(EXPANSION_VISIBLE)
     }
 
     @Test
@@ -124,7 +126,6 @@
         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)
@@ -132,26 +133,26 @@
 
     @Test
     fun testExpansion() {
-        `when`(repository.expansionAmount.value).thenReturn(0.5f)
-        bouncerInteractor.setExpansion(0.6f)
-        verify(repository).setExpansion(0.6f)
+        `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
+        bouncerInteractor.setPanelExpansion(0.6f)
+        verify(repository).setPanelExpansion(0.6f)
         verify(bouncerCallbackInteractor).dispatchExpansionChanged(0.6f)
     }
 
     @Test
     fun testExpansion_fullyShown() {
-        `when`(repository.expansionAmount.value).thenReturn(0.5f)
+        `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
         `when`(repository.startingDisappearAnimation.value).thenReturn(null)
-        bouncerInteractor.setExpansion(EXPANSION_VISIBLE)
+        bouncerInteractor.setPanelExpansion(EXPANSION_VISIBLE)
         verify(falsingCollector).onBouncerShown()
         verify(bouncerCallbackInteractor).dispatchFullyShown()
     }
 
     @Test
     fun testExpansion_fullyHidden() {
-        `when`(repository.expansionAmount.value).thenReturn(0.5f)
+        `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
         `when`(repository.startingDisappearAnimation.value).thenReturn(null)
-        bouncerInteractor.setExpansion(EXPANSION_HIDDEN)
+        bouncerInteractor.setPanelExpansion(EXPANSION_HIDDEN)
         verify(repository).setVisible(false)
         verify(repository).setShow(null)
         verify(falsingCollector).onBouncerHidden()
@@ -161,8 +162,8 @@
 
     @Test
     fun testExpansion_startingToHide() {
-        `when`(repository.expansionAmount.value).thenReturn(EXPANSION_VISIBLE)
-        bouncerInteractor.setExpansion(0.1f)
+        `when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
+        bouncerInteractor.setPanelExpansion(0.1f)
         verify(repository).setStartingToHide(true)
         verify(bouncerCallbackInteractor).dispatchStartingToHide()
     }
@@ -178,8 +179,7 @@
         val onDismissAction = mock(ActivityStarter.OnDismissAction::class.java)
         val cancelAction = mock(Runnable::class.java)
         bouncerInteractor.setDismissAction(onDismissAction, cancelAction)
-        verify(repository)
-            .setOnDismissAction(BouncerCallbackActionsModel(onDismissAction, cancelAction))
+        verify(bouncerViewDelegate).setDismissAction(onDismissAction, cancelAction)
     }
 
     @Test
@@ -234,7 +234,7 @@
     @Test
     fun testIsFullShowing() {
         `when`(repository.isVisible.value).thenReturn(true)
-        `when`(repository.expansionAmount.value).thenReturn(EXPANSION_VISIBLE)
+        `when`(repository.panelExpansionAmount.value).thenReturn(EXPANSION_VISIBLE)
         `when`(repository.startingDisappearAnimation.value).thenReturn(null)
         assertThat(bouncerInteractor.isFullyShowing()).isTrue()
         `when`(repository.isVisible.value).thenReturn(false)
@@ -255,7 +255,7 @@
         assertThat(bouncerInteractor.isInTransit()).isTrue()
         `when`(repository.showingSoon.value).thenReturn(false)
         assertThat(bouncerInteractor.isInTransit()).isFalse()
-        `when`(repository.expansionAmount.value).thenReturn(0.5f)
+        `when`(repository.panelExpansionAmount.value).thenReturn(0.5f)
         assertThat(bouncerInteractor.isInTransit()).isTrue()
     }
 
@@ -269,10 +269,9 @@
 
     @Test
     fun testWillDismissWithAction() {
-        `when`(repository.onDismissAction.value?.onDismissAction)
-            .thenReturn(mock(ActivityStarter.OnDismissAction::class.java))
+        `when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(true)
         assertThat(bouncerInteractor.willDismissWithAction()).isTrue()
-        `when`(repository.onDismissAction.value?.onDismissAction).thenReturn(null)
+        `when`(bouncerViewDelegate.willDismissWithActions()).thenReturn(false)
         assertThat(bouncerInteractor.willDismissWithAction()).isFalse()
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
index 7116cc1..8b6603d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorParameterizedTest.kt
@@ -25,10 +25,14 @@
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.plugins.ActivityStarter
@@ -37,6 +41,8 @@
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.test.runBlockingTest
 import org.junit.Before
 import org.junit.Test
@@ -189,6 +195,8 @@
                     /* startActivity= */ true,
                 ),
             )
+
+        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
@@ -213,10 +221,20 @@
         whenever(expandable.activityLaunchController()).thenReturn(animationController)
 
         homeControls =
-            object :
-                FakeKeyguardQuickAffordanceConfig(
-                    BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
-                ) {}
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
+        val quickAccessWallet =
+            FakeKeyguardQuickAffordanceConfig(
+                BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+            )
+        val qrCodeScanner =
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
+        val quickAffordanceRepository =
+            KeyguardQuickAffordanceRepository(
+                scope = CoroutineScope(IMMEDIATE),
+                backgroundDispatcher = IMMEDIATE,
+                selectionManager = KeyguardQuickAffordanceSelectionManager(),
+                configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
+            )
         underTest =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor = KeyguardInteractor(repository = FakeKeyguardRepository()),
@@ -229,14 +247,8 @@
                                 ),
                             KeyguardQuickAffordancePosition.BOTTOM_END to
                                 listOf(
-                                    object :
-                                        FakeKeyguardQuickAffordanceConfig(
-                                            BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
-                                        ) {},
-                                    object :
-                                        FakeKeyguardQuickAffordanceConfig(
-                                            BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
-                                        ) {},
+                                    quickAccessWallet,
+                                    qrCodeScanner,
                                 ),
                         ),
                     ),
@@ -244,6 +256,11 @@
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
                 activityStarter = activityStarter,
+                featureFlags =
+                    FakeFeatureFlags().apply {
+                        set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+                    },
+                repository = { quickAffordanceRepository },
             )
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
index ae32ba6..3364535 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt
@@ -22,22 +22,31 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.model.KeyguardQuickAffordanceModel
 import com.android.systemui.keyguard.domain.quickaffordance.FakeKeyguardQuickAffordanceRegistry
 import com.android.systemui.keyguard.shared.quickaffordance.ActivationState
 import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancePosition
 import com.android.systemui.plugins.ActivityStarter
 import com.android.systemui.settings.UserTracker
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
 import com.android.systemui.statusbar.policy.KeyguardStateController
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.test.runBlockingTest
 import kotlinx.coroutines.yield
 import org.junit.Before
@@ -47,6 +56,7 @@
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
+@OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
 @RunWith(JUnit4::class)
 class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
@@ -62,6 +72,7 @@
     private lateinit var homeControls: FakeKeyguardQuickAffordanceConfig
     private lateinit var quickAccessWallet: FakeKeyguardQuickAffordanceConfig
     private lateinit var qrCodeScanner: FakeKeyguardQuickAffordanceConfig
+    private lateinit var featureFlags: FakeFeatureFlags
 
     @Before
     fun setUp() {
@@ -71,20 +82,25 @@
         repository.setKeyguardShowing(true)
 
         homeControls =
-            object :
-                FakeKeyguardQuickAffordanceConfig(
-                    BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
-                ) {}
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
         quickAccessWallet =
-            object :
-                FakeKeyguardQuickAffordanceConfig(
-                    BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
-                ) {}
+            FakeKeyguardQuickAffordanceConfig(
+                BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+            )
         qrCodeScanner =
-            object :
-                FakeKeyguardQuickAffordanceConfig(
-                    BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
-                ) {}
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
+
+        val quickAffordanceRepository =
+            KeyguardQuickAffordanceRepository(
+                scope = CoroutineScope(IMMEDIATE),
+                backgroundDispatcher = IMMEDIATE,
+                selectionManager = KeyguardQuickAffordanceSelectionManager(),
+                configs = setOf(homeControls, quickAccessWallet, qrCodeScanner),
+            )
+        featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+            }
 
         underTest =
             KeyguardQuickAffordanceInteractor(
@@ -107,6 +123,8 @@
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
                 activityStarter = activityStarter,
+                featureFlags = featureFlags,
+                repository = { quickAffordanceRepository },
             )
     }
 
@@ -210,6 +228,270 @@
             job.cancel()
         }
 
+    @Test
+    fun select() =
+        runBlocking(IMMEDIATE) {
+            featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
+            homeControls.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            quickAccessWallet.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            qrCodeScanner.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf<String, List<String>>(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+
+            var startConfig: KeyguardQuickAffordanceModel? = null
+            val job1 =
+                underTest
+                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                    .onEach { startConfig = it }
+                    .launchIn(this)
+            var endConfig: KeyguardQuickAffordanceModel? = null
+            val job2 =
+                underTest
+                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                    .onEach { endConfig = it }
+                    .launchIn(this)
+
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
+            yield()
+            yield()
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START +
+                                "::${homeControls.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+                            listOf(homeControls.key),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+
+            underTest.select(
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
+                quickAccessWallet.key
+            )
+            yield()
+            yield()
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START +
+                                "::${quickAccessWallet.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+                            listOf(quickAccessWallet.key),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, qrCodeScanner.key)
+            yield()
+            yield()
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START +
+                                "::${quickAccessWallet.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END +
+                                "::${qrCodeScanner.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
+                            listOf(quickAccessWallet.key),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+                            listOf(qrCodeScanner.key),
+                    )
+                )
+
+            job1.cancel()
+            job2.cancel()
+        }
+
+    @Test
+    fun `unselect - one`() =
+        runBlocking(IMMEDIATE) {
+            featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
+            homeControls.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            quickAccessWallet.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            qrCodeScanner.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+
+            var startConfig: KeyguardQuickAffordanceModel? = null
+            val job1 =
+                underTest
+                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START)
+                    .onEach { startConfig = it }
+                    .launchIn(this)
+            var endConfig: KeyguardQuickAffordanceModel? = null
+            val job2 =
+                underTest
+                    .quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_END)
+                    .onEach { endConfig = it }
+                    .launchIn(this)
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
+            yield()
+            yield()
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key)
+            yield()
+            yield()
+
+            underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
+            yield()
+            yield()
+
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Visible(
+                        configKey =
+                            KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END +
+                                "::${quickAccessWallet.key}",
+                        icon = ICON,
+                        activationState = ActivationState.NotSupported,
+                    )
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+                            listOf(quickAccessWallet.key),
+                    )
+                )
+
+            underTest.unselect(
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                quickAccessWallet.key
+            )
+            yield()
+            yield()
+
+            assertThat(startConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(endConfig)
+                .isEqualTo(
+                    KeyguardQuickAffordanceModel.Hidden,
+                )
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf<String, List<String>>(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+
+            job1.cancel()
+            job2.cancel()
+        }
+
+    @Test
+    fun `unselect - all`() =
+        runBlocking(IMMEDIATE) {
+            featureFlags.set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
+            homeControls.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            quickAccessWallet.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+            qrCodeScanner.setState(
+                KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON)
+            )
+
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key)
+            yield()
+            yield()
+            underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key)
+            yield()
+            yield()
+
+            underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, null)
+            yield()
+            yield()
+
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to
+                            listOf(quickAccessWallet.key),
+                    )
+                )
+
+            underTest.unselect(
+                KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END,
+                null,
+            )
+            yield()
+            yield()
+
+            assertThat(underTest.getSelections())
+                .isEqualTo(
+                    mapOf<String, List<String>>(
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to emptyList(),
+                        KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(),
+                    )
+                )
+        }
+
     companion object {
         private val ICON: Icon = mock {
             whenever(this.contentDescription)
@@ -220,5 +502,6 @@
                 )
         }
         private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337
+        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
index 0424c28..6333b24 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt
@@ -54,9 +54,11 @@
     @Test
     fun `transition collectors receives only appropriate events`() =
         runBlocking(IMMEDIATE) {
-            var goneToAodSteps = mutableListOf<TransitionStep>()
+            var lockscreenToAodSteps = mutableListOf<TransitionStep>()
             val job1 =
-                underTest.goneToAodTransition.onEach { goneToAodSteps.add(it) }.launchIn(this)
+                underTest.lockscreenToAodTransition
+                    .onEach { lockscreenToAodSteps.add(it) }
+                    .launchIn(this)
 
             var aodToLockscreenSteps = mutableListOf<TransitionStep>()
             val job2 =
@@ -70,14 +72,14 @@
             steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
             steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
             steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
-            steps.add(TransitionStep(GONE, AOD, 0f, STARTED))
-            steps.add(TransitionStep(GONE, AOD, 0.1f, RUNNING))
-            steps.add(TransitionStep(GONE, AOD, 0.2f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.1f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.2f, RUNNING))
 
             steps.forEach { repository.sendTransitionStep(it) }
 
             assertThat(aodToLockscreenSteps).isEqualTo(steps.subList(2, 5))
-            assertThat(goneToAodSteps).isEqualTo(steps.subList(5, 8))
+            assertThat(lockscreenToAodSteps).isEqualTo(steps.subList(5, 8))
 
             job1.cancel()
             job2.cancel()
@@ -119,10 +121,8 @@
     fun keyguardStateTests() =
         runBlocking(IMMEDIATE) {
             var finishedSteps = mutableListOf<KeyguardState>()
-            val job1 =
+            val job =
                 underTest.finishedKeyguardState.onEach { finishedSteps.add(it) }.launchIn(this)
-            var startedSteps = mutableListOf<KeyguardState>()
-            val job2 = underTest.startedKeyguardState.onEach { startedSteps.add(it) }.launchIn(this)
 
             val steps = mutableListOf<TransitionStep>()
 
@@ -137,10 +137,60 @@
             steps.forEach { repository.sendTransitionStep(it) }
 
             assertThat(finishedSteps).isEqualTo(listOf(LOCKSCREEN, AOD))
-            assertThat(startedSteps).isEqualTo(listOf(LOCKSCREEN, AOD, GONE))
 
-            job1.cancel()
-            job2.cancel()
+            job.cancel()
+        }
+
+    @Test
+    fun finishedKeyguardTransitionStepTests() =
+        runBlocking(IMMEDIATE) {
+            var finishedSteps = mutableListOf<TransitionStep>()
+            val job =
+                underTest.finishedKeyguardTransitionStep
+                    .onEach { finishedSteps.add(it) }
+                    .launchIn(this)
+
+            val steps = mutableListOf<TransitionStep>()
+
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+            steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
+
+            steps.forEach { repository.sendTransitionStep(it) }
+
+            assertThat(finishedSteps).isEqualTo(listOf(steps[2], steps[5]))
+
+            job.cancel()
+        }
+
+    @Test
+    fun startedKeyguardTransitionStepTests() =
+        runBlocking(IMMEDIATE) {
+            var startedSteps = mutableListOf<TransitionStep>()
+            val job =
+                underTest.startedKeyguardTransitionStep
+                    .onEach { startedSteps.add(it) }
+                    .launchIn(this)
+
+            val steps = mutableListOf<TransitionStep>()
+
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0f, STARTED))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 0.5f, RUNNING))
+            steps.add(TransitionStep(AOD, LOCKSCREEN, 1f, FINISHED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0f, STARTED))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 0.9f, RUNNING))
+            steps.add(TransitionStep(LOCKSCREEN, AOD, 1f, FINISHED))
+            steps.add(TransitionStep(AOD, GONE, 1f, STARTED))
+
+            steps.forEach { repository.sendTransitionStep(it) }
+
+            assertThat(startedSteps).isEqualTo(listOf(steps[0], steps[3], steps[6]))
+
+            job.cancel()
         }
 
     companion object {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
index f73d1ec..78148c4 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
@@ -23,10 +23,14 @@
 import com.android.systemui.animation.Expandable
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.doze.util.BurnInHelperWrapper
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
+import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceSelectionManager
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
+import com.android.systemui.keyguard.data.repository.KeyguardQuickAffordanceRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
 import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
@@ -41,6 +45,8 @@
 import com.google.common.truth.Truth.assertThat
 import kotlin.math.max
 import kotlin.math.min
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.runBlockingTest
@@ -82,20 +88,13 @@
             .thenReturn(RETURNED_BURN_IN_OFFSET)
 
         homeControlsQuickAffordanceConfig =
-            object :
-                FakeKeyguardQuickAffordanceConfig(
-                    BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS
-                ) {}
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS)
         quickAccessWalletAffordanceConfig =
-            object :
-                FakeKeyguardQuickAffordanceConfig(
-                    BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
-                ) {}
+            FakeKeyguardQuickAffordanceConfig(
+                BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET
+            )
         qrCodeScannerAffordanceConfig =
-            object :
-                FakeKeyguardQuickAffordanceConfig(
-                    BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER
-                ) {}
+            FakeKeyguardQuickAffordanceConfig(BuiltInKeyguardQuickAffordanceKeys.QR_CODE_SCANNER)
         registry =
             FakeKeyguardQuickAffordanceRegistry(
                 mapOf(
@@ -116,6 +115,18 @@
         whenever(userTracker.userHandle).thenReturn(mock())
         whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
             .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
+        val quickAffordanceRepository =
+            KeyguardQuickAffordanceRepository(
+                scope = CoroutineScope(IMMEDIATE),
+                backgroundDispatcher = IMMEDIATE,
+                selectionManager = KeyguardQuickAffordanceSelectionManager(),
+                configs =
+                    setOf(
+                        homeControlsQuickAffordanceConfig,
+                        quickAccessWalletAffordanceConfig,
+                        qrCodeScannerAffordanceConfig,
+                    ),
+            )
         underTest =
             KeyguardBottomAreaViewModel(
                 keyguardInteractor = keyguardInteractor,
@@ -127,6 +138,11 @@
                         keyguardStateController = keyguardStateController,
                         userTracker = userTracker,
                         activityStarter = activityStarter,
+                        featureFlags =
+                            FakeFeatureFlags().apply {
+                                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+                            },
+                        repository = { quickAffordanceRepository },
                     ),
                 bottomAreaInteractor = KeyguardBottomAreaInteractor(repository = repository),
                 burnInHelperWrapper = burnInHelperWrapper,
@@ -576,5 +592,6 @@
     companion object {
         private const val DEFAULT_BURN_IN_OFFSET = 5
         private const val RETURNED_BURN_IN_OFFSET = 3
+        private val IMMEDIATE = Dispatchers.Main.immediate
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
index 6adce7a..c1fa9b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/NavigationBarTest.java
@@ -61,6 +61,7 @@
 import android.view.DisplayInfo;
 import android.view.MotionEvent;
 import android.view.View;
+import android.view.ViewRootImpl;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
 import android.view.WindowManager;
@@ -201,6 +202,8 @@
     private WakefulnessLifecycle mWakefulnessLifecycle;
     @Mock
     private Resources mResources;
+    @Mock
+    private ViewRootImpl mViewRootImpl;
     private FakeExecutor mFakeExecutor = new FakeExecutor(new FakeSystemClock());
     private DeviceConfigProxyFake mDeviceConfigProxyFake = new DeviceConfigProxyFake();
 
@@ -227,6 +230,7 @@
         when(mUserContextProvider.createCurrentUserContext(any(Context.class)))
                 .thenReturn(mContext);
         when(mNavigationBarView.getResources()).thenReturn(mResources);
+        when(mNavigationBarView.getViewRootImpl()).thenReturn(mViewRootImpl);
         setupSysuiDependency();
         // This class inflates views that call Dependency.get, thus these injections are still
         // necessary.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
index 2c76be6..b067ee7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest.java
@@ -24,6 +24,7 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
 import android.service.quicksettings.Tile;
 import android.testing.AndroidTestingRunner;
@@ -136,6 +137,20 @@
         assertEquals(mIconView.getColor(s1), mIconView.getColor(s2));
     }
 
+    @Test
+    public void testIconNotAnimatedWhenAllowAnimationsFalse() {
+        ImageView iv = new ImageView(mContext);
+        AnimatedVectorDrawable d = mock(AnimatedVectorDrawable.class);
+        State s = new State();
+        s.icon = mock(Icon.class);
+        when(s.icon.getDrawable(any())).thenReturn(d);
+        when(s.icon.getInvisibleDrawable(any())).thenReturn(d);
+
+        mIconView.updateIcon(iv, s, false);
+
+        verify(d, never()).start();
+    }
+
     private static Drawable.ConstantState fakeConstantState(Drawable otherDrawable) {
         return new Drawable.ConstantState() {
             @Override
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
index 45b4353..c98c1f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java
@@ -486,6 +486,7 @@
                 mSysUiState,
                 () -> mKeyguardBottomAreaViewController,
                 mKeyguardUnlockAnimationController,
+                mKeyguardIndicationController,
                 mNotificationListContainer,
                 mNotificationStackSizeCalculator,
                 mUnlockedScreenOffAnimationController,
@@ -499,8 +500,6 @@
                 () -> {},
                 mNotificationShelfController);
         mNotificationPanelViewController.setHeadsUpManager(mHeadsUpManager);
-        mNotificationPanelViewController.setKeyguardIndicationController(
-                mKeyguardIndicationController);
         ArgumentCaptor<View.OnAttachStateChangeListener> onAttachStateChangeListenerArgumentCaptor =
                 ArgumentCaptor.forClass(View.OnAttachStateChangeListener.class);
         verify(mView, atLeast(1)).addOnAttachStateChangeListener(
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 26a0770..a4a7995 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.java
@@ -152,7 +152,7 @@
 
         // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.isShowingAlternateAuth()).thenReturn(true);
         when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
 
         // THEN we should intercept touch
@@ -165,7 +165,7 @@
 
         // WHEN not showing alt auth, not dozing, drag down helper doesn't want to intercept
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()).thenReturn(false);
+        when(mStatusBarKeyguardViewManager.isShowingAlternateAuth()).thenReturn(false);
         when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
 
         // THEN we shouldn't intercept touch
@@ -178,7 +178,7 @@
 
         // WHEN showing alt auth, not dozing, drag down helper doesn't want to intercept
         when(mStatusBarStateController.isDozing()).thenReturn(false);
-        when(mStatusBarKeyguardViewManager.isShowingAlternateAuthOrAnimating()).thenReturn(true);
+        when(mStatusBarKeyguardViewManager.isShowingAlternateAuth()).thenReturn(true);
         when(mDragDownHelper.onInterceptTouchEvent(any())).thenReturn(false);
 
         // THEN we should handle the touch
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
index 09add65..43c6942 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/PulsingGestureListenerTest.kt
@@ -66,6 +66,8 @@
     private lateinit var dumpManager: DumpManager
     @Mock
     private lateinit var statusBarStateController: StatusBarStateController
+    @Mock
+    private lateinit var shadeLogger: ShadeLogger
 
     private lateinit var tunableCaptor: ArgumentCaptor<Tunable>
     private lateinit var underTest: PulsingGestureListener
@@ -81,6 +83,7 @@
                 centralSurfaces,
                 ambientDisplayConfiguration,
                 statusBarStateController,
+                shadeLogger,
                 tunerService,
                 dumpManager
         )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
index 6fa2174..b17747a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java
@@ -143,7 +143,7 @@
                 mAuthController, mStatusBarStateController, mKeyguardUnlockAnimationController,
                 mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper);
         mBiometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager);
-        mBiometricUnlockController.setBiometricModeListener(mBiometricModeListener);
+        mBiometricUnlockController.addBiometricModeListener(mBiometricModeListener);
     }
 
     @Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
index 5755782..7ce3a67 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java
@@ -500,7 +500,7 @@
                 mKeyguardVieMediatorCallback);
 
         // TODO: we should be able to call mCentralSurfaces.start() and have all the below values
-        // initialized automatically.
+        // initialized automatically and make NPVC private.
         mCentralSurfaces.mNotificationShadeWindowView = mNotificationShadeWindowView;
         mCentralSurfaces.mNotificationPanelViewController = mNotificationPanelViewController;
         mCentralSurfaces.mDozeScrimController = mDozeScrimController;
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
index 9c56c26..6fb6893 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarIconControllerTest.java
@@ -45,7 +45,7 @@
 import com.android.systemui.statusbar.phone.StatusBarSignalPolicy.WifiIconState;
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
-import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel;
+import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 
 import org.junit.Before;
@@ -80,7 +80,7 @@
                 layout,
                 StatusBarLocation.HOME,
                 mock(StatusBarPipelineFlags.class),
-                mock(WifiViewModel.class),
+                mock(WifiUiAdapter.class),
                 mock(MobileUiAdapter.class),
                 mMobileContextProvider,
                 mock(DarkIconDispatcher.class));
@@ -124,14 +124,14 @@
                 LinearLayout group,
                 StatusBarLocation location,
                 StatusBarPipelineFlags statusBarPipelineFlags,
-                WifiViewModel wifiViewModel,
+                WifiUiAdapter wifiUiAdapter,
                 MobileUiAdapter mobileUiAdapter,
                 MobileContextProvider contextProvider,
                 DarkIconDispatcher darkIconDispatcher) {
             super(group,
                     location,
                     statusBarPipelineFlags,
-                    wifiViewModel,
+                    wifiUiAdapter,
                     mobileUiAdapter,
                     contextProvider,
                     darkIconDispatcher);
@@ -172,7 +172,7 @@
             super(group,
                     StatusBarLocation.HOME,
                     mock(StatusBarPipelineFlags.class),
-                    mock(WifiViewModel.class),
+                    mock(WifiUiAdapter.class),
                     mock(MobileUiAdapter.class),
                     contextProvider);
         }
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 0c35659..7166666 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
@@ -307,7 +307,7 @@
 
     @Test
     public void onPanelExpansionChanged_neverTranslatesBouncerWhenLaunchingApp() {
-        when(mCentralSurfaces.isInLaunchTransition()).thenReturn(true);
+        when(mNotificationPanelView.isLaunchTransitionFinished()).thenReturn(true);
         mStatusBarKeyguardViewManager.onPanelExpansionChanged(
                 expansionEvent(
                         /* fraction= */ KeyguardBouncer.EXPANSION_VISIBLE,
@@ -361,7 +361,7 @@
 
     @Test
     public void setOccluded_isInLaunchTransition_onKeyguardOccludedChangedCalled() {
-        when(mCentralSurfaces.isInLaunchTransition()).thenReturn(true);
+        when(mNotificationPanelView.isLaunchTransitionFinished()).thenReturn(true);
         mStatusBarKeyguardViewManager.show(null);
 
         mStatusBarKeyguardViewManager.setOccluded(true /* occluded */, false /* animated */);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
index c584109..37457b3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/wifi/ui/view/ModernStatusBarWifiViewTest.kt
@@ -28,7 +28,6 @@
 import com.android.systemui.statusbar.StatusBarIconView.STATE_DOT
 import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN
 import com.android.systemui.statusbar.StatusBarIconView.STATE_ICON
-import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
 import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
@@ -40,6 +39,7 @@
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.statusbar.pipeline.wifi.domain.interactor.WifiInteractor
 import com.android.systemui.statusbar.pipeline.wifi.shared.WifiConstants
+import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.LocationBasedWifiViewModel
 import com.android.systemui.statusbar.pipeline.wifi.ui.viewmodel.WifiViewModel
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
@@ -70,7 +70,7 @@
     private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var wifiRepository: FakeWifiRepository
     private lateinit var interactor: WifiInteractor
-    private lateinit var viewModel: WifiViewModel
+    private lateinit var viewModel: LocationBasedWifiViewModel
     private lateinit var scope: CoroutineScope
     private lateinit var airplaneModeViewModel: AirplaneModeViewModel
 
@@ -105,23 +105,19 @@
             scope,
             statusBarPipelineFlags,
             wifiConstants,
-        )
+        ).home
     }
 
     @Test
     fun constructAndBind_hasCorrectSlot() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, "slotName", viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, "slotName", viewModel)
 
         assertThat(view.slot).isEqualTo("slotName")
     }
 
     @Test
     fun getVisibleState_icon_returnsIcon() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_ICON, /* animate= */ false)
 
@@ -130,9 +126,7 @@
 
     @Test
     fun getVisibleState_dot_returnsDot() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_DOT, /* animate= */ false)
 
@@ -141,9 +135,7 @@
 
     @Test
     fun getVisibleState_hidden_returnsHidden() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
 
@@ -155,9 +147,7 @@
 
     @Test
     fun setVisibleState_icon_iconShownDotHidden() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_ICON, /* animate= */ false)
 
@@ -172,9 +162,7 @@
 
     @Test
     fun setVisibleState_dot_iconHiddenDotShown() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_DOT, /* animate= */ false)
 
@@ -189,9 +177,7 @@
 
     @Test
     fun setVisibleState_hidden_iconAndDotHidden() {
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         view.setVisibleState(STATE_HIDDEN, /* animate= */ false)
 
@@ -211,9 +197,7 @@
             WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
         )
 
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         ViewUtils.attachView(view)
         testableLooper.processAllMessages()
@@ -230,9 +214,7 @@
             WifiNetworkModel.Active(NETWORK_ID, isValidated = true, level = 2)
         )
 
-        val view = ModernStatusBarWifiView.constructAndBind(
-            context, SLOT_NAME, viewModel, StatusBarLocation.HOME
-        )
+        val view = ModernStatusBarWifiView.constructAndBind(context, SLOT_NAME, viewModel)
 
         ViewUtils.attachView(view)
         testableLooper.processAllMessages()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java
index 4f1fb02..26df03f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/HotspotControllerImplTest.java
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.policy;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
@@ -25,6 +26,7 @@
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
 import static org.mockito.Mockito.when;
 
 import android.net.TetheringManager;
@@ -36,6 +38,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.dump.DumpManager;
 
@@ -96,6 +99,9 @@
         }).when(mWifiManager).registerSoftApCallback(any(Executor.class),
                 any(WifiManager.SoftApCallback.class));
 
+        mContext.getOrCreateTestableResources()
+                .addOverride(R.bool.config_show_wifi_tethering, true);
+
         Handler handler = new Handler(mLooper.getLooper());
 
         mController = new HotspotControllerImpl(mContext, handler, handler, mDumpManager);
@@ -176,4 +182,18 @@
 
         verify(mCallback1).onHotspotAvailabilityChanged(false);
     }
+
+    @Test
+    public void testHotspotSupported_resource_false() {
+        mContext.getOrCreateTestableResources()
+                .addOverride(R.bool.config_show_wifi_tethering, false);
+
+        Handler handler = new Handler(mLooper.getLooper());
+
+        HotspotController controller =
+                new HotspotControllerImpl(mContext, handler, handler, mDumpManager);
+
+        verifyNoMoreInteractions(mTetheringManager);
+        assertFalse(controller.isHotspotSupported());
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
index 120bf79..b485693 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/GuestUserInteractorTest.kt
@@ -23,6 +23,8 @@
 import android.os.UserManager
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.GuestResetOrExitSessionReceiver
+import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.user.data.repository.FakeUserRepository
@@ -55,6 +57,8 @@
     @Mock private lateinit var dismissDialog: () -> Unit
     @Mock private lateinit var selectUser: (Int) -> Unit
     @Mock private lateinit var switchUser: (Int) -> Unit
+    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
+    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
 
     private lateinit var underTest: GuestUserInteractor
 
@@ -87,10 +91,18 @@
                         repository = repository,
                     ),
                 uiEventLogger = uiEventLogger,
+                resumeSessionReceiver = resumeSessionReceiver,
+                resetOrExitSessionReceiver = resetOrExitSessionReceiver,
             )
     }
 
     @Test
+    fun `registers broadcast receivers`() {
+        verify(resumeSessionReceiver).register()
+        verify(resetOrExitSessionReceiver).register()
+    }
+
+    @Test
     fun `onDeviceBootCompleted - allowed to add - create guest`() =
         runBlocking(IMMEDIATE) {
             setAllowedToAdd()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
index 1680c36c..58f5531 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/UserInteractorTest.kt
@@ -21,6 +21,8 @@
 import android.app.admin.DevicePolicyManager
 import android.os.UserManager
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.GuestResetOrExitSessionReceiver
+import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FakeFeatureFlags
 import com.android.systemui.flags.Flags
@@ -48,6 +50,8 @@
     @Mock protected lateinit var devicePolicyManager: DevicePolicyManager
     @Mock protected lateinit var uiEventLogger: UiEventLogger
     @Mock protected lateinit var dialogShower: UserSwitchDialogController.DialogShower
+    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
+    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
 
     protected lateinit var underTest: UserInteractor
 
@@ -107,6 +111,8 @@
                         devicePolicyManager = devicePolicyManager,
                         refreshUsersScheduler = refreshUsersScheduler,
                         uiEventLogger = uiEventLogger,
+                        resumeSessionReceiver = resumeSessionReceiver,
+                        resetOrExitSessionReceiver = resetOrExitSessionReceiver,
                     )
             )
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
index c12a868..116023a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/UserSwitcherViewModelTest.kt
@@ -23,6 +23,8 @@
 import android.os.UserManager
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
+import com.android.systemui.GuestResetOrExitSessionReceiver
+import com.android.systemui.GuestResumeSessionReceiver
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.common.shared.model.Text
 import com.android.systemui.flags.FakeFeatureFlags
@@ -69,6 +71,8 @@
     @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
     @Mock private lateinit var devicePolicyManager: DevicePolicyManager
     @Mock private lateinit var uiEventLogger: UiEventLogger
+    @Mock private lateinit var resumeSessionReceiver: GuestResumeSessionReceiver
+    @Mock private lateinit var resetOrExitSessionReceiver: GuestResetOrExitSessionReceiver
 
     private lateinit var underTest: UserSwitcherViewModel
 
@@ -104,6 +108,8 @@
                 devicePolicyManager = devicePolicyManager,
                 refreshUsersScheduler = refreshUsersScheduler,
                 uiEventLogger = uiEventLogger,
+                resumeSessionReceiver = resumeSessionReceiver,
+                resetOrExitSessionReceiver = resetOrExitSessionReceiver,
             )
 
         underTest =
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 11178db..627bd09 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.data.repository
 
 import com.android.systemui.common.shared.model.Position
+import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import kotlinx.coroutines.flow.Flow
@@ -52,6 +53,12 @@
     private val _wakefulnessState = MutableStateFlow(WakefulnessModel.ASLEEP)
     override val wakefulnessState: Flow<WakefulnessModel> = _wakefulnessState
 
+    private val _isBouncerShowing = MutableStateFlow(false)
+    override val isBouncerShowing: Flow<Boolean> = _isBouncerShowing
+
+    private val _biometricUnlockState = MutableStateFlow(BiometricUnlockModel.NONE)
+    override val biometricUnlockState: Flow<BiometricUnlockModel> = _biometricUnlockState
+
     override fun isKeyguardShowing(): Boolean {
         return _isKeyguardShowing.value
     }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
index 23c7a61..2d6d29a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeStatusBarIconController.java
@@ -62,7 +62,11 @@
     }
 
     @Override
-    public void setSignalIcon(String slot, WifiIconState state) {
+    public void setWifiIcon(String slot, WifiIconState state) {
+    }
+
+    @Override
+    public void setNewWifiIcon() {
     }
 
     @Override
diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
index 252dcfc..4430bb4b 100644
--- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -26,6 +26,7 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
 import android.annotation.UserIdInt;
 import android.app.ActivityManager;
 import android.app.ActivityManagerInternal;
@@ -254,11 +255,13 @@
     private boolean mSafeMode;
     private int mMaxWidgetBitmapMemory;
     private boolean mIsProviderInfoPersisted;
+    private boolean mIsCombinedBroadcastEnabled;
 
     AppWidgetServiceImpl(Context context) {
         mContext = context;
     }
 
+    @RequiresPermission(android.Manifest.permission.READ_DEVICE_CONFIG)
     public void onStart() {
         mPackageManager = AppGlobals.getPackageManager();
         mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
@@ -277,6 +280,8 @@
         mIsProviderInfoPersisted = !ActivityManager.isLowRamDeviceStatic()
                 && DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI,
                 SystemUiDeviceConfigFlags.PERSISTS_WIDGET_PROVIDER_INFO, true);
+        mIsCombinedBroadcastEnabled = DeviceConfig.getBoolean(NAMESPACE_SYSTEMUI,
+            SystemUiDeviceConfigFlags.COMBINED_BROADCAST_ENABLED, true);
         if (DEBUG_PROVIDER_INFO_CACHE && !mIsProviderInfoPersisted) {
             Slog.d(TAG, "App widget provider info will not be persisted on this device");
         }
@@ -1123,16 +1128,16 @@
 
             final int widgetCount = provider.widgets.size();
             if (widgetCount == 1) {
-                // Tell the provider that it's ready.
-                sendEnableIntentLocked(provider);
+                // If we are binding the very first widget from a provider, we will send
+                // a combined broadcast or 2 separate broadcasts to tell the provider that
+                // it's ready, and we need them to provide the update now.
+                sendEnableAndUpdateIntentLocked(provider, new int[]{appWidgetId});
+            } else {
+                // For any widget other then the first one, we just send update intent
+                // as we normally would.
+                sendUpdateIntentLocked(provider, new int[]{appWidgetId});
             }
 
-            // Send an update now -- We need this update now, and just for this appWidgetId.
-            // It's less critical when the next one happens, so when we schedule the next one,
-            // we add updatePeriodMillis to its start time. That time will have some slop,
-            // but that's okay.
-            sendUpdateIntentLocked(provider, new int[] {appWidgetId});
-
             // Schedule the future updates.
             registerForBroadcastsLocked(provider, getWidgetIds(provider.widgets));
 
@@ -2361,6 +2366,22 @@
         cancelBroadcastsLocked(provider);
     }
 
+    private void sendEnableAndUpdateIntentLocked(@NonNull Provider p, int[] appWidgetIds) {
+        final boolean canSendCombinedBroadcast = mIsCombinedBroadcastEnabled && p.info != null
+                && p.info.isExtendedFromAppWidgetProvider;
+        if (!canSendCombinedBroadcast) {
+            // If this function is called by mistake, send two separate broadcasts instead
+            sendEnableIntentLocked(p);
+            sendUpdateIntentLocked(p, appWidgetIds);
+            return;
+        }
+
+        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLE_AND_UPDATE);
+        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+        intent.setComponent(p.id.componentName);
+        sendBroadcastAsUser(intent, p.id.getProfile());
+    }
+
     private void sendEnableIntentLocked(Provider p) {
         Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLED);
         intent.setComponent(p.id.componentName);
@@ -2852,7 +2873,6 @@
                 if (provider.widgets.size() > 0) {
                     Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                             "appwidget init " + provider.id.componentName.getPackageName());
-                    sendEnableIntentLocked(provider);
                     provider.widgets.forEach(widget -> {
                         widget.trackingUpdate = true;
                         Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
@@ -2861,7 +2881,7 @@
                         Log.i(TAG, "Widget update scheduled on unlock " + widget.toString());
                     });
                     int[] appWidgetIds = getWidgetIds(provider.widgets);
-                    sendUpdateIntentLocked(provider, appWidgetIds);
+                    sendEnableAndUpdateIntentLocked(provider, appWidgetIds);
                     registerForBroadcastsLocked(provider, appWidgetIds);
                     Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                 }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index df5113b..85d6f293 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -2886,10 +2886,13 @@
                 || event == Event.ACTIVITY_DESTROYED)) {
             contentCaptureService.notifyActivityEvent(userId, activity, event);
         }
-        // TODO(b/201234353): Move the logic to client side.
-        if (mVoiceInteractionManagerProvider != null && (event == Event.ACTIVITY_PAUSED
-                || event == Event.ACTIVITY_RESUMED || event == Event.ACTIVITY_STOPPED)) {
-            mVoiceInteractionManagerProvider.notifyActivityEventChanged();
+        // Currently we have move most of logic to the client side. When the activity lifecycle
+        // event changed, the client side will notify the VoiceInteractionManagerService. But
+        // when the application process died, the VoiceInteractionManagerService will miss the
+        // activity lifecycle event changed, so we still need ACTIVITY_DESTROYED event here to
+        // know if the activity has been destroyed.
+        if (mVoiceInteractionManagerProvider != null && event == Event.ACTIVITY_DESTROYED) {
+            mVoiceInteractionManagerProvider.notifyActivityDestroyed(appToken);
         }
     }
 
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
index ca4b747..dbe971f3 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java
@@ -100,9 +100,7 @@
                 owner, cookie, requireConfirmation, sensorId, logger, biometricContext,
                 isStrongBiometric, null /* taskStackListener */, lockoutCache,
                 allowBackgroundAuthentication,
-                context.getResources().getBoolean(
-                        com.android.internal.R.bool.system_server_plays_face_haptics)
-                /* shouldVibrate */,
+                false /* shouldVibrate */,
                 isKeyguardBypassEnabled);
         setRequestId(requestId);
         mUsageStats = usageStats;
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
index 9baca98..91eec7d 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java
@@ -74,7 +74,7 @@
         super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
                 owner, cookie, requireConfirmation, sensorId, logger, biometricContext,
                 isStrongBiometric, null /* taskStackListener */,
-                lockoutTracker, allowBackgroundAuthentication, true /* shouldVibrate */,
+                lockoutTracker, allowBackgroundAuthentication, false /* shouldVibrate */,
                 isKeyguardBypassEnabled);
         setRequestId(requestId);
         mUsageStats = usageStats;
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
index a778b57..9aecf78 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java
@@ -69,7 +69,6 @@
 class FingerprintAuthenticationClient extends AuthenticationClient<AidlSession>
         implements Udfps, LockoutConsumer, PowerPressHandler {
     private static final String TAG = "FingerprintAuthenticationClient";
-    private static final int MESSAGE_IGNORE_AUTH = 1;
     private static final int MESSAGE_AUTH_SUCCESS = 2;
     private static final int MESSAGE_FINGER_UP = 3;
     @NonNull
@@ -136,7 +135,7 @@
                 taskStackListener,
                 lockoutCache,
                 allowBackgroundAuthentication,
-                true /* shouldVibrate */,
+                false /* shouldVibrate */,
                 false /* isKeyguardBypassEnabled */);
         setRequestId(requestId);
         mLockoutCache = lockoutCache;
@@ -235,12 +234,6 @@
                 () -> {
                     long delay = 0;
                     if (authenticated && mSensorProps.isAnySidefpsType()) {
-                        if (mHandler.hasMessages(MESSAGE_IGNORE_AUTH)) {
-                            Slog.i(TAG, "(sideFPS) Ignoring auth due to recent power press");
-                            onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0,
-                                    true);
-                            return;
-                        }
                         delay = isKeyguard() ? mWaitForAuthKeyguard : mWaitForAuthBp;
 
                         if (mSideFpsLastAcquireStartTime != -1) {
@@ -497,16 +490,12 @@
         if (mSensorProps.isAnySidefpsType()) {
             Slog.i(TAG, "(sideFPS): onPowerPressed");
             mHandler.post(() -> {
-                if (mHandler.hasMessages(MESSAGE_AUTH_SUCCESS)) {
-                    Slog.i(TAG, "(sideFPS): Ignoring auth in queue");
-                    mHandler.removeMessages(MESSAGE_AUTH_SUCCESS);
-                    // Do not call onError() as that will send an additional callback to coex.
-                    onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true);
-                }
-                mHandler.removeMessages(MESSAGE_IGNORE_AUTH);
-                mHandler.postDelayed(() -> {
-                }, MESSAGE_IGNORE_AUTH, mIgnoreAuthFor);
-
+                Slog.i(TAG, "(sideFPS): finishing auth");
+                // Ignore auths after a power has been detected
+                mHandler.removeMessages(MESSAGE_AUTH_SUCCESS);
+                // Do not call onError() as that will send an additional callback to coex.
+                onErrorInternal(BiometricConstants.BIOMETRIC_ERROR_POWER_PRESSED, 0, true);
+                mSensorOverlays.hide(getSensorId());
             });
         }
     }
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
index 7ed1a51..0d620fd 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java
@@ -81,7 +81,7 @@
         super(context, lazyDaemon, token, listener, targetUserId, operationId, restricted,
                 owner, cookie, requireConfirmation, sensorId, logger, biometricContext,
                 isStrongBiometric, taskStackListener, lockoutTracker, allowBackgroundAuthentication,
-                true /* shouldVibrate */, false /* isKeyguardBypassEnabled */);
+                false /* shouldVibrate */, false /* isKeyguardBypassEnabled */);
         setRequestId(requestId);
         mLockoutFrameworkImpl = lockoutTracker;
         mSensorOverlays = new SensorOverlays(udfpsOverlayController, sidefpsController);
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 5589673..e4aa5e5 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -157,7 +157,7 @@
         mDozeConfig = new AmbientDisplayConfiguration(mContext);
         mUiEventLogger = new UiEventLoggerImpl();
         mDreamUiEventLogger = new DreamUiEventLoggerImpl(
-                mContext.getResources().getString(R.string.config_loggable_dream_prefix));
+                mContext.getResources().getStringArray(R.array.config_loggable_dream_prefixes));
         AmbientDisplayConfiguration adc = new AmbientDisplayConfiguration(mContext);
         mAmbientDisplayComponent = ComponentName.unflattenFromString(adc.ambientDisplayComponent());
         mDreamsOnlyEnabledForSystemUser =
diff --git a/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java b/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java
index 26ca74a..96ebcbb 100644
--- a/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java
+++ b/services/core/java/com/android/server/dreams/DreamUiEventLoggerImpl.java
@@ -26,10 +26,10 @@
  * @hide
  */
 public class DreamUiEventLoggerImpl implements DreamUiEventLogger {
-    final String mLoggableDreamPrefix;
+    private final String[] mLoggableDreamPrefixes;
 
-    DreamUiEventLoggerImpl(String loggableDreamPrefix) {
-        mLoggableDreamPrefix = loggableDreamPrefix;
+    DreamUiEventLoggerImpl(String[] loggableDreamPrefixes) {
+        mLoggableDreamPrefixes = loggableDreamPrefixes;
     }
 
     @Override
@@ -38,13 +38,20 @@
         if (eventID <= 0) {
             return;
         }
-        final boolean isFirstPartyDream =
-                mLoggableDreamPrefix.isEmpty() ? false : dreamComponentName.startsWith(
-                        mLoggableDreamPrefix);
         FrameworkStatsLog.write(FrameworkStatsLog.DREAM_UI_EVENT_REPORTED,
                 /* uid = 1 */ 0,
                 /* event_id = 2 */ eventID,
                 /* instance_id = 3 */ 0,
-                /* dream_component_name = 4 */ isFirstPartyDream ? dreamComponentName : "other");
+                /* dream_component_name = 4 */
+                isFirstPartyDream(dreamComponentName) ? dreamComponentName : "other");
+    }
+
+    private boolean isFirstPartyDream(String dreamComponentName) {
+        for (int i = 0; i < mLoggableDreamPrefixes.length; ++i) {
+            if (dreamComponentName.startsWith(mLoggableDreamPrefixes[i])) {
+                return true;
+            }
+        }
+        return false;
     }
 }
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 2eb2cf6..e86f34b 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -812,7 +812,6 @@
     StartingData mStartingData;
     WindowState mStartingWindow;
     StartingSurfaceController.StartingSurface mStartingSurface;
-    boolean startingDisplayed;
     boolean startingMoved;
 
     /** The last set {@link DropInputMode} for this activity surface. */
@@ -821,13 +820,6 @@
     /** Whether the input to this activity will be dropped during the current playing animation. */
     private boolean mIsInputDroppedForAnimation;
 
-    /**
-     * If it is non-null, it requires all activities who have the same starting data to be drawn
-     * to remove the starting window.
-     * TODO(b/189385912): Remove starting window related fields after migrating them to task.
-     */
-    private StartingData mSharedStartingData;
-
     boolean mHandleExitSplashScreen;
     @TransferSplashScreenState
     int mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE;
@@ -1200,14 +1192,11 @@
             pw.print(" firstWindowDrawn="); pw.print(firstWindowDrawn);
             pw.print(" mIsExiting="); pw.println(mIsExiting);
         }
-        if (mSharedStartingData != null) {
-            pw.println(prefix + "mSharedStartingData=" + mSharedStartingData);
-        }
-        if (mStartingWindow != null || mStartingSurface != null
-                || startingDisplayed || startingMoved || mVisibleSetFromTransferredStartingWindow) {
+        if (mStartingWindow != null || mStartingData != null || mStartingSurface != null
+                || startingMoved || mVisibleSetFromTransferredStartingWindow) {
             pw.print(prefix); pw.print("startingWindow="); pw.print(mStartingWindow);
             pw.print(" startingSurface="); pw.print(mStartingSurface);
-            pw.print(" startingDisplayed="); pw.print(startingDisplayed);
+            pw.print(" startingDisplayed="); pw.print(isStartingWindowDisplayed());
             pw.print(" startingMoved="); pw.print(startingMoved);
             pw.println(" mVisibleSetFromTransferredStartingWindow="
                     + mVisibleSetFromTransferredStartingWindow);
@@ -2690,13 +2679,23 @@
         }
     }
 
+    boolean isStartingWindowDisplayed() {
+        final StartingData data = mStartingData != null ? mStartingData : task != null
+                ? task.mSharedStartingData : null;
+        return data != null && data.mIsDisplayed;
+    }
+
     /** Called when the starting window is added to this activity. */
     void attachStartingWindow(@NonNull WindowState startingWindow) {
         startingWindow.mStartingData = mStartingData;
         mStartingWindow = startingWindow;
-        // The snapshot type may have called associateStartingDataWithTask().
-        if (mStartingData != null && mStartingData.mAssociatedTask != null) {
-            attachStartingSurfaceToAssociatedTask();
+        if (mStartingData != null) {
+            if (mStartingData.mAssociatedTask != null) {
+                // The snapshot type may have called associateStartingDataWithTask().
+                attachStartingSurfaceToAssociatedTask();
+            } else if (isEmbedded()) {
+                associateStartingWindowWithTaskIfNeeded();
+            }
         }
     }
 
@@ -2711,11 +2710,7 @@
     /** Called when the starting window is not added yet but its data is known to fill the task. */
     private void associateStartingDataWithTask() {
         mStartingData.mAssociatedTask = task;
-        task.forAllActivities(r -> {
-            if (r.mVisibleRequested && !r.firstWindowDrawn) {
-                r.mSharedStartingData = mStartingData;
-            }
-        });
+        task.mSharedStartingData = mStartingData;
     }
 
     /** Associates and attaches an added starting window to the current task. */
@@ -2746,10 +2741,8 @@
 
     void removeStartingWindowAnimation(boolean prepareAnimation) {
         mTransferringSplashScreenState = TRANSFER_SPLASH_SCREEN_IDLE;
-        if (mSharedStartingData != null) {
-            mSharedStartingData.mAssociatedTask.forAllActivities(r -> {
-                r.mSharedStartingData = null;
-            });
+        if (task != null) {
+            task.mSharedStartingData = null;
         }
         if (mStartingWindow == null) {
             if (mStartingData != null) {
@@ -2772,7 +2765,6 @@
             mStartingData = null;
             mStartingSurface = null;
             mStartingWindow = null;
-            startingDisplayed = false;
             if (surface == null) {
                 ProtoLog.v(WM_DEBUG_STARTING_WINDOW, "startingWindow was set but "
                         + "startingSurface==null, couldn't remove");
@@ -4257,7 +4249,7 @@
      * @return {@code true} if starting window is in app's hierarchy.
      */
     boolean hasStartingWindow() {
-        if (startingDisplayed || mStartingData != null) {
+        if (mStartingData != null) {
             return true;
         }
         for (int i = mChildren.size() - 1; i >= 0; i--) {
@@ -4355,10 +4347,7 @@
 
                 // Transfer the starting window over to the new token.
                 mStartingData = fromActivity.mStartingData;
-                mSharedStartingData = fromActivity.mSharedStartingData;
                 mStartingSurface = fromActivity.mStartingSurface;
-                startingDisplayed = fromActivity.startingDisplayed;
-                fromActivity.startingDisplayed = false;
                 mStartingWindow = tStartingWindow;
                 reportedVisible = fromActivity.reportedVisible;
                 fromActivity.mStartingData = null;
@@ -4424,7 +4413,6 @@
             ProtoLog.v(WM_DEBUG_STARTING_WINDOW,
                     "Moving pending starting from %s to %s", fromActivity, this);
             mStartingData = fromActivity.mStartingData;
-            mSharedStartingData = fromActivity.mSharedStartingData;
             fromActivity.mStartingData = null;
             fromActivity.startingMoved = true;
             scheduleAddStartingWindow();
@@ -6534,14 +6522,11 @@
         // Remove starting window directly if is in a pure task. Otherwise if it is associated with
         // a task (e.g. nested task fragment), then remove only if all visible windows in the task
         // are drawn.
-        final Task associatedTask =
-                mSharedStartingData != null ? mSharedStartingData.mAssociatedTask : null;
+        final Task associatedTask = task.mSharedStartingData != null ? task : null;
         if (associatedTask == null) {
             removeStartingWindow();
-        } else if (associatedTask.getActivity(r -> r.mVisibleRequested && !r.firstWindowDrawn
-                // Don't block starting window removal if an Activity can't be a starting window
-                // target.
-                && r.mSharedStartingData != null) == null) {
+        } else if (associatedTask.getActivity(
+                r -> r.mVisibleRequested && !r.firstWindowDrawn) == null) {
             // The last drawn activity may not be the one that owns the starting window.
             final ActivityRecord r = associatedTask.topActivityContainsStartingWindow();
             if (r != null) {
@@ -6756,7 +6741,6 @@
         if (mLastTransactionSequence != mWmService.mTransactionSequence) {
             mLastTransactionSequence = mWmService.mTransactionSequence;
             mNumDrawnWindows = 0;
-            startingDisplayed = false;
 
             // There is the main base application window, even if it is exiting, wait for it
             mNumInterestingWindows = findMainWindow(false /* includeStartingApp */) != null ? 1 : 0;
@@ -6800,9 +6784,9 @@
                         isInterestingAndDrawn = true;
                     }
                 }
-            } else if (w.isDrawn()) {
+            } else if (mStartingData != null && w.isDrawn()) {
                 // The starting window for this container is drawn.
-                startingDisplayed = true;
+                mStartingData.mIsDisplayed = true;
             }
         }
 
@@ -7550,7 +7534,8 @@
 
         ProtoLog.v(WM_DEBUG_ANIM, "Animation done in %s"
                 + ": reportedVisible=%b okToDisplay=%b okToAnimate=%b startingDisplayed=%b",
-                this, reportedVisible, okToDisplay(), okToAnimate(), startingDisplayed);
+                this, reportedVisible, okToDisplay(), okToAnimate(),
+                isStartingWindowDisplayed());
 
         // clean up thumbnail window
         if (mThumbnail != null) {
@@ -9649,7 +9634,7 @@
         if (mStartingWindow != null) {
             mStartingWindow.writeIdentifierToProto(proto, STARTING_WINDOW);
         }
-        proto.write(STARTING_DISPLAYED, startingDisplayed);
+        proto.write(STARTING_DISPLAYED, isStartingWindowDisplayed());
         proto.write(STARTING_MOVED, startingMoved);
         proto.write(VISIBLE_SET_FROM_TRANSFERRED_STARTING_WINDOW,
                 mVisibleSetFromTransferredStartingWindow);
diff --git a/services/core/java/com/android/server/wm/AppTransitionController.java b/services/core/java/com/android/server/wm/AppTransitionController.java
index 9c95e31..671524b 100644
--- a/services/core/java/com/android/server/wm/AppTransitionController.java
+++ b/services/core/java/com/android/server/wm/AppTransitionController.java
@@ -1229,11 +1229,11 @@
             ProtoLog.v(WM_DEBUG_APP_TRANSITIONS,
                     "Check opening app=%s: allDrawn=%b startingDisplayed=%b "
                             + "startingMoved=%b isRelaunching()=%b startingWindow=%s",
-                    activity, activity.allDrawn, activity.startingDisplayed,
+                    activity, activity.allDrawn, activity.isStartingWindowDisplayed(),
                     activity.startingMoved, activity.isRelaunching(),
                     activity.mStartingWindow);
             final boolean allDrawn = activity.allDrawn && !activity.isRelaunching();
-            if (!allDrawn && !activity.startingDisplayed && !activity.startingMoved) {
+            if (!allDrawn && !activity.isStartingWindowDisplayed() && !activity.startingMoved) {
                 return false;
             }
             if (allDrawn) {
diff --git a/services/core/java/com/android/server/wm/Dimmer.java b/services/core/java/com/android/server/wm/Dimmer.java
index e7ab63e..13a1cb6 100644
--- a/services/core/java/com/android/server/wm/Dimmer.java
+++ b/services/core/java/com/android/server/wm/Dimmer.java
@@ -216,14 +216,10 @@
             return;
         }
 
-        if (container != null) {
-            // The dim method is called from WindowState.prepareSurfaces(), which is always called
-            // in the correct Z from lowest Z to highest. This ensures that the dim layer is always
-            // relative to the highest Z layer with a dim.
-            t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
-        } else {
-            t.setLayer(d.mDimLayer, Integer.MAX_VALUE);
-        }
+        // The dim method is called from WindowState.prepareSurfaces(), which is always called
+        // in the correct Z from lowest Z to highest. This ensures that the dim layer is always
+        // relative to the highest Z layer with a dim.
+        t.setRelativeLayer(d.mDimLayer, container.getSurfaceControl(), relativeLayer);
         t.setAlpha(d.mDimLayer, alpha);
         t.setBackgroundBlurRadius(d.mDimLayer, blurRadius);
 
@@ -231,32 +227,6 @@
     }
 
     /**
-     * Finish a dim started by dimAbove in the case there was no call to dimAbove.
-     *
-     * @param t A Transaction in which to finish the dim.
-     */
-    void stopDim(SurfaceControl.Transaction t) {
-        if (mDimState != null) {
-            t.hide(mDimState.mDimLayer);
-            mDimState.isVisible = false;
-            mDimState.mDontReset = false;
-        }
-    }
-
-    /**
-     * Place a Dim above the entire host container. The caller is responsible for calling stopDim to
-     * remove this effect. If the Dim can be assosciated with a particular child of the host
-     * consider using the other variant of dimAbove which ties the Dim lifetime to the child
-     * lifetime more explicitly.
-     *
-     * @param t     A transaction in which to apply the Dim.
-     * @param alpha The alpha at which to Dim.
-     */
-    void dimAbove(SurfaceControl.Transaction t, float alpha) {
-        dim(t, null, 1, alpha, 0);
-    }
-
-    /**
      * Place a dim above the given container, which should be a child of the host container.
      * for each call to {@link WindowContainer#prepareSurfaces} the Dim state will be reset
      * and the child should call dimAbove again to request the Dim to continue.
diff --git a/services/core/java/com/android/server/wm/LockTaskController.java b/services/core/java/com/android/server/wm/LockTaskController.java
index f11c2a7..dcb7fe3 100644
--- a/services/core/java/com/android/server/wm/LockTaskController.java
+++ b/services/core/java/com/android/server/wm/LockTaskController.java
@@ -604,7 +604,10 @@
                 getDevicePolicyManager().notifyLockTaskModeChanged(false, null, userId);
             }
             if (oldLockTaskModeState == LOCK_TASK_MODE_PINNED) {
-                getStatusBarService().showPinningEnterExitToast(false /* entering */);
+                final IStatusBarService statusBarService = getStatusBarService();
+                if (statusBarService != null) {
+                    statusBarService.showPinningEnterExitToast(false /* entering */);
+                }
             }
             mWindowManager.onLockTaskStateChanged(mLockTaskModeState);
         } catch (RemoteException ex) {
@@ -619,7 +622,10 @@
     void showLockTaskToast() {
         if (mLockTaskModeState == LOCK_TASK_MODE_PINNED) {
             try {
-                getStatusBarService().showPinningEscapeToast();
+                final IStatusBarService statusBarService = getStatusBarService();
+                if (statusBarService != null) {
+                    statusBarService.showPinningEscapeToast();
+                }
             } catch (RemoteException e) {
                 Slog.e(TAG, "Failed to send pinning escape toast", e);
             }
@@ -727,7 +733,10 @@
         // When lock task starts, we disable the status bars.
         try {
             if (lockTaskModeState == LOCK_TASK_MODE_PINNED) {
-                getStatusBarService().showPinningEnterExitToast(true /* entering */);
+                final IStatusBarService statusBarService = getStatusBarService();
+                if (statusBarService != null) {
+                    statusBarService.showPinningEnterExitToast(true /* entering */);
+                }
             }
             mWindowManager.onLockTaskStateChanged(lockTaskModeState);
             mLockTaskModeState = lockTaskModeState;
diff --git a/services/core/java/com/android/server/wm/StartingData.java b/services/core/java/com/android/server/wm/StartingData.java
index fbee343..300a894 100644
--- a/services/core/java/com/android/server/wm/StartingData.java
+++ b/services/core/java/com/android/server/wm/StartingData.java
@@ -38,6 +38,9 @@
      */
     Task mAssociatedTask;
 
+    /** Whether the starting window is drawn. */
+    boolean mIsDisplayed;
+
     protected StartingData(WindowManagerService service, int typeParams) {
         mService = service;
         mTypeParams = typeParams;
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index eba49bb..66d7af9 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -358,6 +358,13 @@
 
     int mLockTaskUid = -1;  // The uid of the application that called startLockTask().
 
+    /**
+     * If non-null, the starting window should cover the associated task. It is assigned when the
+     * parent activity of starting window is put in a partial area of the task. This field will be
+     * cleared when all visible activities in this task are drawn.
+     */
+    StartingData mSharedStartingData;
+
     /** The process that had previously hosted the root activity of this task.
      * Used to know that we should try harder to keep this process around, in case the
      * user wants to return to it. */
@@ -3688,6 +3695,9 @@
         if (mRootProcess != null) {
             pw.print(prefix); pw.print("mRootProcess="); pw.println(mRootProcess);
         }
+        if (mSharedStartingData != null) {
+            pw.println(prefix + "mSharedStartingData=" + mSharedStartingData);
+        }
         pw.print(prefix); pw.print("taskId=" + mTaskId);
         pw.println(" rootTaskId=" + getRootTaskId());
         pw.print(prefix); pw.println("hasChildPipActivity=" + (mChildPipActivity != null));
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index d178676..879c7e2 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -1890,10 +1890,10 @@
     RemoteAnimationTarget createRemoteAnimationTarget(
             RemoteAnimationController.RemoteAnimationRecord record) {
         final ActivityRecord activity = record.getMode() == RemoteAnimationTarget.MODE_OPENING
-                // There may be a trampoline activity without window on top of the existing task
-                // which is moving to front. Exclude the finishing activity so the window of next
-                // activity can be chosen to create the animation target.
-                ? getTopNonFinishingActivity()
+                // There may be a launching (e.g. trampoline or embedded) activity without a window
+                // on top of the existing task which is moving to front. Exclude finishing activity
+                // so the window of next activity can be chosen to create the animation target.
+                ? getActivity(r -> !r.finishing && r.hasChild())
                 : getTopMostActivity();
         return activity != null ? activity.createRemoteAnimationTarget(record) : null;
     }
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index ac720be..9c9d751 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -8684,11 +8684,12 @@
         h.ownerPid = callingPid;
 
         if (region == null) {
-            h.replaceTouchableRegionWithCrop = true;
+            h.replaceTouchableRegionWithCrop(null);
         } else {
             h.touchableRegion.set(region);
+            h.replaceTouchableRegionWithCrop = false;
+            h.setTouchableRegionCrop(surface);
         }
-        h.setTouchableRegionCrop(null /* use the input surface's bounds */);
 
         final SurfaceControl.Transaction t = mTransactionFactory.get();
         t.setInputWindowInfo(surface, h);
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 744bf0a..d4c1abf 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -1842,8 +1842,8 @@
      * @return {@code true} if one or more windows have been displayed, else false.
      */
     boolean hasAppShownWindows() {
-        return mActivityRecord != null
-                && (mActivityRecord.firstWindowDrawn || mActivityRecord.startingDisplayed);
+        return mActivityRecord != null && (mActivityRecord.firstWindowDrawn
+                || mActivityRecord.isStartingWindowDisplayed());
     }
 
     @Override
diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java
index 6e16b5d..a0ba8fd 100644
--- a/services/core/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java
@@ -374,13 +374,6 @@
     }
 
     void destroySurfaceLocked(SurfaceControl.Transaction t) {
-        final ActivityRecord activity = mWin.mActivityRecord;
-        if (activity != null) {
-            if (mWin == activity.mStartingWindow) {
-                activity.startingDisplayed = false;
-            }
-        }
-
         if (mSurfaceController == null) {
             return;
         }
diff --git a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
index 7610b7c..b33e22f 100644
--- a/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/appwidget/AppWidgetServiceImplTest.java
@@ -164,7 +164,7 @@
     }
 
     public void testRequestPinAppWidget() {
-        ComponentName provider = new ComponentName(mTestContext, DummyAppWidget.class);
+        ComponentName provider = new ComponentName(mTestContext, TestAppWidgetProvider.class);
         // Set up users.
         when(mMockShortcutService.requestPinAppWidget(anyString(),
                 any(AppWidgetProviderInfo.class), eq(null), eq(null), anyInt()))
@@ -289,6 +289,16 @@
         assertEquals(4, updates.size());
     }
 
+    public void testReceiveBroadcastBehavior_enableAndUpdate() {
+        TestAppWidgetProvider testAppWidgetProvider = new TestAppWidgetProvider();
+        Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_ENABLE_AND_UPDATE);
+
+        testAppWidgetProvider.onReceive(mTestContext, intent);
+
+        assertTrue(testAppWidgetProvider.isBehaviorSuccess());
+    }
+
+
     public void testUpdatesReceived_queueNotEmpty_multipleWidgetIdProvided() {
         int widgetId = setupHostAndWidget();
         int widgetId2 = bindNewWidget();
@@ -385,7 +395,7 @@
     }
 
     private int bindNewWidget() {
-        ComponentName provider = new ComponentName(mTestContext, DummyAppWidget.class);
+        ComponentName provider = new ComponentName(mTestContext, TestAppWidgetProvider.class);
         int widgetId = mService.allocateAppWidgetId(mPkgName, HOST_ID);
         assertTrue(mManager.bindAppWidgetIdIfAllowed(widgetId, provider));
         assertEquals(provider, mManager.getAppWidgetInfo(widgetId).provider);
diff --git a/services/tests/servicestests/src/com/android/server/appwidget/DummyAppWidget.java b/services/tests/servicestests/src/com/android/server/appwidget/DummyAppWidget.java
deleted file mode 100644
index fd99b21..0000000
--- a/services/tests/servicestests/src/com/android/server/appwidget/DummyAppWidget.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License
- */
-
-package com.android.server.appwidget;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-
-/**
- * Placeholder widget for testing
- */
-public class DummyAppWidget extends BroadcastReceiver {
-
-    @Override
-    public void onReceive(Context context, Intent intent) {
-    }
-}
diff --git a/services/tests/servicestests/src/com/android/server/appwidget/TestAppWidgetProvider.java b/services/tests/servicestests/src/com/android/server/appwidget/TestAppWidgetProvider.java
new file mode 100644
index 0000000..6c11a68
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/appwidget/TestAppWidgetProvider.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.appwidget;
+
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+
+/**
+ * Placeholder widget for testing
+ */
+public class TestAppWidgetProvider extends AppWidgetProvider {
+    private boolean mEnabled;
+    private boolean mUpdated;
+
+    TestAppWidgetProvider() {
+        super();
+        mEnabled = false;
+        mUpdated = false;
+    }
+
+    public boolean isBehaviorSuccess() {
+        return mEnabled && mUpdated;
+    }
+
+    @Override
+    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
+            int[] appWidgetids) {
+        mUpdated = true;
+    }
+
+    @Override
+    public void onEnabled(Context context) {
+        mEnabled = true;
+    }
+}
diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
index 606f486..cd4af0a 100644
--- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
+++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java
@@ -616,6 +616,20 @@
         verify(mCallback).onClientFinished(any(), eq(true));
     }
 
+    @Test
+    public void sideFpsPowerPressCancelsIsntantly() throws Exception {
+        when(mSensorProps.isAnySidefpsType()).thenReturn(true);
+
+        final FingerprintAuthenticationClient client = createClient(1);
+        client.start(mCallback);
+
+        client.onPowerPressed();
+        mLooper.dispatchAll();
+
+        verify(mCallback, never()).onClientFinished(any(), eq(true));
+        verify(mCallback).onClientFinished(any(), eq(false));
+    }
+
     private FingerprintAuthenticationClient createClient() throws RemoteException {
         return createClient(100 /* version */, true /* allowBackgroundAuthentication */);
     }
diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
index 462957a..8a0a4f7 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java
@@ -2886,6 +2886,7 @@
         fragmentSetup.accept(taskFragment1, new Rect(0, 0, width / 2, height));
         task.addChild(taskFragment1, POSITION_TOP);
         assertEquals(task, activity1.mStartingData.mAssociatedTask);
+        assertEquals(activity1.mStartingData, task.mSharedStartingData);
 
         final TaskFragment taskFragment2 = new TaskFragment(
                 mAtm, null /* fragmentToken */, false /* createdByOrganizer */);
@@ -2905,7 +2906,6 @@
 
         verify(activity1.getSyncTransaction()).reparent(eq(startingWindow.mSurfaceControl),
                 eq(task.mSurfaceControl));
-        assertEquals(activity1.mStartingData, startingWindow.mStartingData);
         assertEquals(task.mSurfaceControl, startingWindow.getAnimationLeashParent());
         assertEquals(taskFragment1.getBounds(), activity1.getBounds());
         // The activity was resized by task fragment, but starting window must still cover the task.
@@ -2916,6 +2916,7 @@
         activity1.onFirstWindowDrawn(activityWindow);
         activity2.onFirstWindowDrawn(activityWindow);
         assertNull(activity1.mStartingWindow);
+        assertNull(task.mSharedStartingData);
     }
 
     @Test
@@ -2991,10 +2992,10 @@
         final WindowManager.LayoutParams attrs =
                 new WindowManager.LayoutParams(TYPE_APPLICATION_STARTING);
         final TestWindowState startingWindow = createWindowState(attrs, activity);
-        activity.startingDisplayed = true;
+        activity.mStartingData = mock(StartingData.class);
         activity.addWindow(startingWindow);
         assertTrue("Starting window should be present", activity.hasStartingWindow());
-        activity.startingDisplayed = false;
+        activity.mStartingData = null;
         assertTrue("Starting window should be present", activity.hasStartingWindow());
 
         activity.removeChild(startingWindow);
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
index 513791d..0332c4b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionControllerTest.java
@@ -1300,6 +1300,8 @@
         activity.allDrawn = true;
         // Skip manipulate the SurfaceControl.
         doNothing().when(activity).setDropInputMode(anyInt());
+        // Assume the activity contains a window.
+        doReturn(true).when(activity).hasChild();
         // Make sure activity can create remote animation target.
         doReturn(mock(RemoteAnimationTarget.class)).when(activity).createRemoteAnimationTarget(
                 any());
diff --git a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
index f61effa..d55e53c 100644
--- a/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/AppTransitionTests.java
@@ -321,7 +321,6 @@
         final ActivityRecord activity2 = createActivityRecord(dc2);
 
         activity1.allDrawn = true;
-        activity1.startingDisplayed = true;
         activity1.startingMoved = true;
 
         // Simulate activity resume / finish flows to prepare app transition & set visibility,
diff --git a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
index 55a7c1b..befe4e8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DimmerTests.java
@@ -24,7 +24,6 @@
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
 import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_DIMMER;
 
-import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -139,34 +138,12 @@
     }
 
     @Test
-    public void testDimAboveNoChildCreatesSurface() {
-        final float alpha = 0.8f;
-        mDimmer.dimAbove(mTransaction, alpha);
-
-        SurfaceControl dimLayer = getDimLayer();
-
-        assertNotNull("Dimmer should have created a surface", dimLayer);
-
-        verify(mTransaction).setAlpha(dimLayer, alpha);
-        verify(mTransaction).setLayer(dimLayer, Integer.MAX_VALUE);
-    }
-
-    @Test
-    public void testDimAboveNoChildRedundantlyUpdatesAlphaOnExistingSurface() {
-        float alpha = 0.8f;
-        mDimmer.dimAbove(mTransaction, alpha);
-        final SurfaceControl firstSurface = getDimLayer();
-
-        alpha = 0.9f;
-        mDimmer.dimAbove(mTransaction, alpha);
-
-        assertEquals(firstSurface, getDimLayer());
-        verify(mTransaction).setAlpha(firstSurface, 0.9f);
-    }
-
-    @Test
     public void testUpdateDimsAppliesCrop() {
-        mDimmer.dimAbove(mTransaction, 0.8f);
+        TestWindowContainer child = new TestWindowContainer(mWm);
+        mHost.addChild(child, 0);
+
+        final float alpha = 0.8f;
+        mDimmer.dimAbove(mTransaction, child, alpha);
 
         int width = 100;
         int height = 300;
@@ -178,17 +155,6 @@
     }
 
     @Test
-    public void testDimAboveNoChildNotReset() {
-        mDimmer.dimAbove(mTransaction, 0.8f);
-        SurfaceControl dimLayer = getDimLayer();
-        mDimmer.resetDimStates();
-
-        mDimmer.updateDims(mTransaction, new Rect());
-        verify(mTransaction).show(getDimLayer());
-        verify(mTransaction, never()).remove(dimLayer);
-    }
-
-    @Test
     public void testDimAboveWithChildCreatesSurfaceAboveChild() {
         TestWindowContainer child = new TestWindowContainer(mWm);
         mHost.addChild(child, 0);
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
index 352d8d1..bc5c9ec 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java
@@ -172,11 +172,11 @@
         mAmInternal.setVoiceInteractionManagerProvider(
                 new ActivityManagerInternal.VoiceInteractionManagerProvider() {
                     @Override
-                    public void notifyActivityEventChanged() {
+                    public void notifyActivityDestroyed(IBinder activityToken) {
                         if (DEBUG) {
-                            Slog.d(TAG, "call notifyActivityEventChanged");
+                            Slog.d(TAG, "notifyActivityDestroyed activityToken=" + activityToken);
                         }
-                        mServiceStub.notifyActivityEventChanged();
+                        mServiceStub.notifyActivityDestroyed(activityToken);
                     }
                 });
     }
@@ -447,11 +447,12 @@
             return mImpl.supportsLocalVoiceInteraction();
         }
 
-        void notifyActivityEventChanged() {
+        void notifyActivityDestroyed(@NonNull IBinder activityToken) {
             synchronized (this) {
-                if (mImpl == null) return;
+                if (mImpl == null || activityToken == null) return;
 
-                Binder.withCleanCallingIdentity(() -> mImpl.notifyActivityEventChangedLocked());
+                Binder.withCleanCallingIdentity(
+                        () -> mImpl.notifyActivityDestroyedLocked(activityToken));
             }
         }
 
@@ -1223,6 +1224,16 @@
             }
         }
 
+        @Override
+        public void notifyActivityEventChanged(@NonNull IBinder activityToken, int type) {
+            synchronized (this) {
+                if (mImpl == null || activityToken == null) {
+                    return;
+                }
+                Binder.withCleanCallingIdentity(
+                        () -> mImpl.notifyActivityEventChangedLocked(activityToken, type));
+            }
+        }
         //----------------- Hotword Detection/Validation APIs --------------------------------//
 
         @Override
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
index b9793ca..fabab25 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerServiceImpl.java
@@ -520,9 +520,23 @@
         mActiveSession.stopListeningVisibleActivityChangedLocked();
     }
 
-    public void notifyActivityEventChangedLocked() {
+    public void notifyActivityDestroyedLocked(@NonNull IBinder activityToken) {
         if (DEBUG) {
-            Slog.d(TAG, "notifyActivityEventChangedLocked");
+            Slog.d(TAG, "notifyActivityDestroyedLocked activityToken=" + activityToken);
+        }
+        if (mActiveSession == null || !mActiveSession.mShown) {
+            if (DEBUG) {
+                Slog.d(TAG, "notifyActivityDestroyedLocked not allowed on no session or"
+                        + " hidden session");
+            }
+            return;
+        }
+        mActiveSession.notifyActivityDestroyedLocked(activityToken);
+    }
+
+    public void notifyActivityEventChangedLocked(@NonNull IBinder activityToken, int type) {
+        if (DEBUG) {
+            Slog.d(TAG, "notifyActivityEventChangedLocked type=" + type);
         }
         if (mActiveSession == null || !mActiveSession.mShown) {
             if (DEBUG) {
@@ -531,7 +545,7 @@
             }
             return;
         }
-        mActiveSession.notifyActivityEventChangedLocked();
+        mActiveSession.notifyActivityEventChangedLocked(activityToken, type);
     }
 
     public void updateStateLocked(
diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
index ae9be8c..b24337f 100644
--- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
+++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionSessionConnection.java
@@ -29,6 +29,7 @@
 import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUCTURE;
 import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_TASK_ID;
 
+import android.annotation.NonNull;
 import android.app.ActivityManager;
 import android.app.ActivityTaskManager;
 import android.app.AppOpsManager;
@@ -59,6 +60,7 @@
 import android.service.voice.VisibleActivityInfo;
 import android.service.voice.VoiceInteractionService;
 import android.service.voice.VoiceInteractionSession;
+import android.util.ArrayMap;
 import android.util.Slog;
 import android.view.IWindowManager;
 
@@ -128,7 +130,11 @@
     private boolean mListeningVisibleActivity;
     private final ScheduledExecutorService mScheduledExecutorService =
             Executors.newSingleThreadScheduledExecutor();
-    private final List<VisibleActivityInfo> mVisibleActivityInfos = new ArrayList<>();
+    // Records the visible activity information the system has already called onVisible, without
+    // confirming the result of callback. When activity visible state is changed, we use this to
+    // determine to call onVisible or onInvisible to assistant application.
+    private final ArrayMap<IBinder, VisibleActivityInfo> mVisibleActivityInfoForToken =
+            new ArrayMap<>();
     private final PowerManagerInternal mPowerManagerInternal;
     private final LowPowerStandbyControllerInternal mLowPowerStandbyControllerInternal;
     private final Runnable mRemoveFromLowPowerStandbyAllowlistRunnable =
@@ -530,7 +536,7 @@
 
     public void cancelLocked(boolean finishTask) {
         mListeningVisibleActivity = false;
-        mVisibleActivityInfos.clear();
+        mVisibleActivityInfoForToken.clear();
         hideLocked();
         mCanceled = true;
         if (mBound) {
@@ -608,17 +614,24 @@
         if (DEBUG) {
             Slog.d(TAG, "startListeningVisibleActivityChangedLocked");
         }
-        mListeningVisibleActivity = true;
-        mVisibleActivityInfos.clear();
 
-        mScheduledExecutorService.execute(() -> {
-            if (DEBUG) {
-                Slog.d(TAG, "call handleVisibleActivitiesLocked from enable listening");
-            }
-            synchronized (mLock) {
-                handleVisibleActivitiesLocked();
-            }
-        });
+        if (!mShown || mCanceled || mSession == null) {
+            return;
+        }
+
+        mListeningVisibleActivity = true;
+        mVisibleActivityInfoForToken.clear();
+
+        // It should only need to report which activities are visible
+        final ArrayMap<IBinder, VisibleActivityInfo> newVisibleActivityInfos =
+                getTopVisibleActivityInfosLocked();
+
+        if (newVisibleActivityInfos == null || newVisibleActivityInfos.isEmpty()) {
+            return;
+        }
+        notifyVisibleActivitiesChangedLocked(newVisibleActivityInfos,
+                VisibleActivityInfo.TYPE_ACTIVITY_ADDED);
+        mVisibleActivityInfoForToken.putAll(newVisibleActivityInfos);
     }
 
     void stopListeningVisibleActivityChangedLocked() {
@@ -626,12 +639,13 @@
             Slog.d(TAG, "stopListeningVisibleActivityChangedLocked");
         }
         mListeningVisibleActivity = false;
-        mVisibleActivityInfos.clear();
+        mVisibleActivityInfoForToken.clear();
     }
 
-    void notifyActivityEventChangedLocked() {
+    void notifyActivityEventChangedLocked(@NonNull IBinder activityToken, int type) {
         if (DEBUG) {
-            Slog.d(TAG, "notifyActivityEventChangedLocked");
+            Slog.d(TAG, "notifyActivityEventChangedLocked activityToken=" + activityToken
+                    + ", type=" + type);
         }
         if (!mListeningVisibleActivity) {
             if (DEBUG) {
@@ -640,99 +654,139 @@
             return;
         }
         mScheduledExecutorService.execute(() -> {
-            if (DEBUG) {
-                Slog.d(TAG, "call handleVisibleActivitiesLocked from activity event");
-            }
             synchronized (mLock) {
-                handleVisibleActivitiesLocked();
+                handleVisibleActivitiesLocked(activityToken, type);
             }
         });
     }
 
-    private List<VisibleActivityInfo> getVisibleActivityInfosLocked() {
+    private ArrayMap<IBinder, VisibleActivityInfo> getTopVisibleActivityInfosLocked() {
         if (DEBUG) {
-            Slog.d(TAG, "getVisibleActivityInfosLocked");
+            Slog.d(TAG, "getTopVisibleActivityInfosLocked");
         }
         List<ActivityAssistInfo> allVisibleActivities =
                 LocalServices.getService(ActivityTaskManagerInternal.class)
                         .getTopVisibleActivities();
         if (DEBUG) {
-            Slog.d(TAG,
-                    "getVisibleActivityInfosLocked: allVisibleActivities=" + allVisibleActivities);
+            Slog.d(TAG, "getTopVisibleActivityInfosLocked: allVisibleActivities="
+                    + allVisibleActivities);
         }
-        if (allVisibleActivities == null || allVisibleActivities.isEmpty()) {
+        if (allVisibleActivities.isEmpty()) {
             Slog.w(TAG, "no visible activity");
             return null;
         }
         final int count = allVisibleActivities.size();
-        final List<VisibleActivityInfo> visibleActivityInfos = new ArrayList<>(count);
+        final ArrayMap<IBinder, VisibleActivityInfo> visibleActivityInfoArrayMap =
+                new ArrayMap<>(count);
         for (int i = 0; i < count; i++) {
             ActivityAssistInfo info = allVisibleActivities.get(i);
             if (DEBUG) {
-                Slog.d(TAG, " : activityToken=" + info.getActivityToken()
+                Slog.d(TAG, "ActivityAssistInfo : activityToken=" + info.getActivityToken()
                         + ", assistToken=" + info.getAssistToken()
                         + ", taskId=" + info.getTaskId());
             }
-            visibleActivityInfos.add(
+            visibleActivityInfoArrayMap.put(info.getActivityToken(),
                     new VisibleActivityInfo(info.getTaskId(), info.getAssistToken()));
         }
-        return visibleActivityInfos;
+        return visibleActivityInfoArrayMap;
     }
 
-    private void handleVisibleActivitiesLocked() {
+    // TODO(b/242359988): Split this method up
+    private void handleVisibleActivitiesLocked(@NonNull IBinder activityToken, int type) {
         if (DEBUG) {
-            Slog.d(TAG, "handleVisibleActivitiesLocked");
+            Slog.d(TAG, "handleVisibleActivitiesLocked activityToken=" + activityToken
+                    + ", type=" + type);
         }
-        if (mSession == null) {
-            return;
-        }
-        if (!mShown || !mListeningVisibleActivity || mCanceled) {
-            return;
-        }
-        final List<VisibleActivityInfo> newVisibleActivityInfos = getVisibleActivityInfosLocked();
 
-        if (newVisibleActivityInfos == null || newVisibleActivityInfos.isEmpty()) {
-            notifyVisibleActivitiesChangedLocked(mVisibleActivityInfos,
-                    VisibleActivityInfo.TYPE_ACTIVITY_REMOVED);
-            mVisibleActivityInfos.clear();
+        if (!mListeningVisibleActivity) {
+            if (DEBUG) {
+                Slog.d(TAG, "not enable listening visible activity");
+            }
             return;
         }
-        if (mVisibleActivityInfos.isEmpty()) {
-            notifyVisibleActivitiesChangedLocked(newVisibleActivityInfos,
-                    VisibleActivityInfo.TYPE_ACTIVITY_ADDED);
-            mVisibleActivityInfos.addAll(newVisibleActivityInfos);
+        if (!mShown || mCanceled || mSession == null) {
             return;
         }
 
-        final List<VisibleActivityInfo> addedActivities = new ArrayList<>();
-        final List<VisibleActivityInfo> removedActivities = new ArrayList<>();
+        // We use this local variable to determine to call onVisible or onInvisible.
+        boolean notifyOnVisible = false;
+        VisibleActivityInfo notifyVisibleActivityInfo = null;
 
-        removedActivities.addAll(mVisibleActivityInfos);
-        for (int i = 0; i < newVisibleActivityInfos.size(); i++) {
-            final VisibleActivityInfo candidateVisibleActivityInfo = newVisibleActivityInfos.get(i);
-            if (!removedActivities.isEmpty() && removedActivities.contains(
-                    candidateVisibleActivityInfo)) {
-                removedActivities.remove(candidateVisibleActivityInfo);
-            } else {
-                addedActivities.add(candidateVisibleActivityInfo);
+        if (type == VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_START
+                || type == VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_RESUME) {
+            // It seems that the onStart is unnecessary. But if we have it, the assistant
+            // application can request the directActions early. Even if we have the onStart,
+            // we still need the onResume because it is possible that the activity goes to
+            // onResume from onPause with invisible before the activity goes to onStop from
+            // onPause.
+
+            // Check if we have reported this activity as visible. If we have reported it as
+            // visible, do nothing.
+            if (mVisibleActivityInfoForToken.containsKey(activityToken)) {
+                return;
+            }
+
+            // Before reporting this activity as visible, we need to make sure the activity
+            // is really visible.
+            notifyVisibleActivityInfo = getVisibleActivityInfoFromTopVisibleActivity(
+                    activityToken);
+            if (notifyVisibleActivityInfo == null) {
+                return;
+            }
+            notifyOnVisible = true;
+        } else if (type == VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_PAUSE) {
+            // For the onPause stage, the Activity is not necessarily invisible now, so we need
+            // to check its state.
+            // Note: After syncing with Activity owner, before the onPause is called, the
+            // visibility state has been updated.
+            notifyVisibleActivityInfo = getVisibleActivityInfoFromTopVisibleActivity(
+                    activityToken);
+            if (notifyVisibleActivityInfo != null) {
+                return;
+            }
+
+            // Also make sure we previously reported this Activity as visible.
+            notifyVisibleActivityInfo = mVisibleActivityInfoForToken.get(activityToken);
+            if (notifyVisibleActivityInfo == null) {
+                return;
+            }
+        } else if (type == VoiceInteractionSession.VOICE_INTERACTION_ACTIVITY_EVENT_STOP) {
+            // For the onStop stage, the activity is in invisible state. We only need to consider if
+            // we have reported this activity as visible. If we have reported it as visible, we
+            // need to report it as invisible.
+            // Why we still need onStop? Because it is possible that the activity is in a visible
+            // state during onPause stage, when the activity enters onStop from onPause, we may
+            // need to notify onInvisible.
+            // Note: After syncing with Activity owner, before the onStop is called, the
+            // visibility state has been updated.
+            notifyVisibleActivityInfo = mVisibleActivityInfoForToken.get(activityToken);
+            if (notifyVisibleActivityInfo == null) {
+                return;
+            }
+        } else {
+            Slog.w(TAG, "notifyActivityEventChangedLocked unexpected type=" + type);
+            return;
+        }
+
+        try {
+            mSession.notifyVisibleActivityInfoChanged(notifyVisibleActivityInfo,
+                    notifyOnVisible ? VisibleActivityInfo.TYPE_ACTIVITY_ADDED
+                            : VisibleActivityInfo.TYPE_ACTIVITY_REMOVED);
+        } catch (RemoteException e) {
+            if (DEBUG) {
+                Slog.w(TAG, "handleVisibleActivitiesLocked RemoteException : " + e);
             }
         }
 
-        if (!addedActivities.isEmpty()) {
-            notifyVisibleActivitiesChangedLocked(addedActivities,
-                    VisibleActivityInfo.TYPE_ACTIVITY_ADDED);
+        if (notifyOnVisible) {
+            mVisibleActivityInfoForToken.put(activityToken, notifyVisibleActivityInfo);
+        } else {
+            mVisibleActivityInfoForToken.remove(activityToken);
         }
-        if (!removedActivities.isEmpty()) {
-            notifyVisibleActivitiesChangedLocked(removedActivities,
-                    VisibleActivityInfo.TYPE_ACTIVITY_REMOVED);
-        }
-
-        mVisibleActivityInfos.clear();
-        mVisibleActivityInfos.addAll(newVisibleActivityInfos);
     }
 
     private void notifyVisibleActivitiesChangedLocked(
-            List<VisibleActivityInfo> visibleActivityInfos, int type) {
+            ArrayMap<IBinder, VisibleActivityInfo> visibleActivityInfos, int type) {
         if (visibleActivityInfos == null || visibleActivityInfos.isEmpty()) {
             return;
         }
@@ -741,7 +795,7 @@
         }
         try {
             for (int i = 0; i < visibleActivityInfos.size(); i++) {
-                mSession.notifyVisibleActivityInfoChanged(visibleActivityInfos.get(i), type);
+                mSession.notifyVisibleActivityInfoChanged(visibleActivityInfos.valueAt(i), type);
             }
         } catch (RemoteException e) {
             if (DEBUG) {
@@ -754,6 +808,51 @@
         }
     }
 
+    private VisibleActivityInfo getVisibleActivityInfoFromTopVisibleActivity(
+            @NonNull IBinder activityToken) {
+        final ArrayMap<IBinder, VisibleActivityInfo> visibleActivityInfos =
+                getTopVisibleActivityInfosLocked();
+        if (visibleActivityInfos == null) {
+            return null;
+        }
+        return visibleActivityInfos.get(activityToken);
+    }
+
+    void notifyActivityDestroyedLocked(@NonNull IBinder activityToken) {
+        if (DEBUG) {
+            Slog.d(TAG, "notifyActivityDestroyedLocked activityToken=" + activityToken);
+        }
+        if (!mListeningVisibleActivity) {
+            if (DEBUG) {
+                Slog.d(TAG, "not enable listening visible activity");
+            }
+            return;
+        }
+        mScheduledExecutorService.execute(() -> {
+            synchronized (mLock) {
+                if (!mListeningVisibleActivity) {
+                    return;
+                }
+                if (!mShown || mCanceled || mSession == null) {
+                    return;
+                }
+
+                VisibleActivityInfo visibleActivityInfo = mVisibleActivityInfoForToken.remove(
+                        activityToken);
+                if (visibleActivityInfo != null) {
+                    try {
+                        mSession.notifyVisibleActivityInfoChanged(visibleActivityInfo,
+                                VisibleActivityInfo.TYPE_ACTIVITY_REMOVED);
+                    } catch (RemoteException e) {
+                        if (DEBUG) {
+                            Slog.w(TAG, "notifyVisibleActivityInfoChanged RemoteException : " + e);
+                        }
+                    }
+                }
+            }
+        });
+    }
+
     private void removeFromLowPowerStandbyAllowlist() {
         synchronized (mLock) {
             if (mLowPowerStandbyAllowlisted) {