Merge "Fix disabled state of CallStyle buttons to make them look/act disabled" into tm-qpr-dev
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 37ce0d2..f1a3931 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4739,8 +4739,14 @@
                             }
                             final ArraySet<Pair<String, Integer>> triggerPackages =
                                     new ArraySet<>();
+                            final SparseIntArray countsPerUid = new SparseIntArray();
+                            final SparseIntArray wakeupCountsPerUid = new SparseIntArray();
                             for (int i = 0; i < triggerList.size(); i++) {
                                 final Alarm a = triggerList.get(i);
+                                increment(countsPerUid, a.uid);
+                                if (a.wakeup) {
+                                    increment(wakeupCountsPerUid, a.uid);
+                                }
                                 if (mConstants.USE_TARE_POLICY) {
                                     if (!isExemptFromTare(a)) {
                                         triggerPackages.add(Pair.create(
@@ -4761,7 +4767,8 @@
                             }
                             rescheduleKernelAlarmsLocked();
                             updateNextAlarmClockLocked();
-                            MetricsHelper.pushAlarmBatchDelivered(triggerList.size(), wakeUps);
+                            logAlarmBatchDelivered(
+                                    triggerList.size(), wakeUps, countsPerUid, wakeupCountsPerUid);
                         }
                     }
 
@@ -4776,6 +4783,32 @@
         }
     }
 
+    private static void increment(SparseIntArray array, int key) {
+        final int index = array.indexOfKey(key);
+        if (index >= 0) {
+            array.setValueAt(index, array.valueAt(index) + 1);
+        } else {
+            array.put(key, 1);
+        }
+    }
+
+    private void logAlarmBatchDelivered(
+            int alarms,
+            int wakeups,
+            SparseIntArray countsPerUid,
+            SparseIntArray wakeupCountsPerUid) {
+        final int[] uids = new int[countsPerUid.size()];
+        final int[] countsArray = new int[countsPerUid.size()];
+        final int[] wakeupCountsArray = new int[countsPerUid.size()];
+        for (int i = 0; i < countsPerUid.size(); i++) {
+            uids[i] = countsPerUid.keyAt(i);
+            countsArray[i] = countsPerUid.valueAt(i);
+            wakeupCountsArray[i] = wakeupCountsPerUid.get(uids[i], 0);
+        }
+        MetricsHelper.pushAlarmBatchDelivered(
+                alarms, wakeups, uids, countsArray, wakeupCountsArray);
+    }
+
     /**
      * Attribute blame for a WakeLock.
      *
@@ -5695,12 +5728,7 @@
     }
 
     private void incrementAlarmCount(int uid) {
-        final int uidIndex = mAlarmsPerUid.indexOfKey(uid);
-        if (uidIndex >= 0) {
-            mAlarmsPerUid.setValueAt(uidIndex, mAlarmsPerUid.valueAt(uidIndex) + 1);
-        } else {
-            mAlarmsPerUid.put(uid, 1);
-        }
+        increment(mAlarmsPerUid, uid);
     }
 
     /**
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/MetricsHelper.java b/apex/jobscheduler/service/java/com/android/server/alarm/MetricsHelper.java
index 75ed616..2923cfd 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/MetricsHelper.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/MetricsHelper.java
@@ -111,10 +111,14 @@
                 ActivityManager.processStateAmToProto(callerProcState));
     }
 
-    static void pushAlarmBatchDelivered(int numAlarms, int wakeups) {
+    static void pushAlarmBatchDelivered(
+            int numAlarms, int wakeups, int[] uids, int[] alarmsPerUid, int[] wakeupAlarmsPerUid) {
         FrameworkStatsLog.write(
                 FrameworkStatsLog.ALARM_BATCH_DELIVERED,
                 numAlarms,
-                wakeups);
+                wakeups,
+                uids,
+                alarmsPerUid,
+                wakeupAlarmsPerUid);
     }
 }
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index c8e1131..b231934 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -8630,6 +8630,8 @@
 
         mInsetsController.dump(prefix, writer);
 
+        mOnBackInvokedDispatcher.dump(prefix, writer);
+
         writer.println(prefix + "View Hierarchy:");
         dumpViewHierarchy(innerPrefix, writer, mView);
     }
diff --git a/core/java/android/window/ImeOnBackInvokedDispatcher.java b/core/java/android/window/ImeOnBackInvokedDispatcher.java
index a0bd7f7..34b75a4 100644
--- a/core/java/android/window/ImeOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ImeOnBackInvokedDispatcher.java
@@ -211,6 +211,12 @@
         IOnBackInvokedCallback getIOnBackInvokedCallback() {
             return mIOnBackInvokedCallback;
         }
+
+        @Override
+        public String toString() {
+            return "ImeCallback=ImeOnBackInvokedCallback@" + mId
+                    + " Callback=" + mIOnBackInvokedCallback;
+        }
     }
 
     /**
diff --git a/core/java/android/window/ProxyOnBackInvokedDispatcher.java b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
index 49acde9..eb3bcae 100644
--- a/core/java/android/window/ProxyOnBackInvokedDispatcher.java
+++ b/core/java/android/window/ProxyOnBackInvokedDispatcher.java
@@ -179,16 +179,7 @@
                 return;
             }
             clearCallbacksOnDispatcher();
-            if (actualDispatcher instanceof ProxyOnBackInvokedDispatcher) {
-                // We don't want to nest ProxyDispatchers, so if we are given on, we unwrap its
-                // actual dispatcher.
-                // This can happen when an Activity is recreated but the Window is preserved (e.g.
-                // when going from split-screen back to single screen)
-                mActualDispatcher =
-                        ((ProxyOnBackInvokedDispatcher) actualDispatcher).mActualDispatcher;
-            } else {
-                mActualDispatcher = actualDispatcher;
-            }
+            mActualDispatcher = actualDispatcher;
             transferCallbacksToDispatcher();
         }
     }
diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java
index bfa3447..2b5e16f 100644
--- a/core/java/android/window/WindowOnBackInvokedDispatcher.java
+++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java
@@ -27,6 +27,7 @@
 import android.view.IWindow;
 import android.view.IWindowSession;
 
+import java.io.PrintWriter;
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -221,6 +222,26 @@
     @NonNull
     private static final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
 
+    /**
+     * Dump information about this WindowOnBackInvokedDispatcher
+     * @param prefix the prefix that will be prepended to each line of the produced output
+     * @param writer the writer that will receive the resulting text
+     */
+    public void dump(String prefix, PrintWriter writer) {
+        String innerPrefix = prefix + "    ";
+        writer.println(prefix + "WindowOnBackDispatcher:");
+        if (mAllCallbacks.isEmpty()) {
+            writer.println(prefix + "<None>");
+            return;
+        }
+
+        writer.println(innerPrefix + "Top Callback: " + getTopCallback());
+        writer.println(innerPrefix + "Callbacks: ");
+        mAllCallbacks.forEach((callback, priority) -> {
+            writer.println(innerPrefix + "  Callback: " + callback + " Priority=" + priority);
+        });
+    }
+
     static class OnBackInvokedCallbackWrapper extends IOnBackInvokedCallback.Stub {
         private final WeakReference<OnBackInvokedCallback> mCallback;
 
diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
index f724e55..c946db1 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java
@@ -46,6 +46,38 @@
  */
 public class SystemUiSystemPropertiesFlags {
 
+    /** The teamfood flag allows multiple features to be opted into at once. */
+    public static final Flag TEAMFOOD = devFlag("persist.sysui.teamfood");
+
+    /**
+     * Flags related to notification features
+     */
+    public static final class NotificationFlags {
+
+        /**
+         * FOR DEVELOPMENT / TESTING ONLY!!!
+         * Forcibly demote *ALL* FSI notifications as if no apps have the app op permission.
+         * NOTE: enabling this implies SHOW_STICKY_HUN_FOR_DENIED_FSI in SystemUI
+         */
+        public static final Flag FSI_FORCE_DEMOTE =
+                devFlag("persist.sysui.notification.fsi_force_demote");
+
+        /** Gating the feature which shows FSI-denied notifications as Sticky HUNs */
+        public static final Flag SHOW_STICKY_HUN_FOR_DENIED_FSI =
+                devFlag("persist.sysui.notification.show_sticky_hun_for_denied_fsi");
+
+        /** Gating the ability for users to dismiss ongoing event notifications */
+        public static final Flag ALLOW_DISMISS_ONGOING =
+                devFlag("persist.sysui.notification.ongoing_dismissal");
+
+        /** Gating the redaction of OTP notifications on the lockscreen */
+        public static final Flag OTP_REDACTION =
+                devFlag("persist.sysui.notification.otp_redaction");
+
+    }
+
+    //// == End of flags.  Everything below this line is the implementation. == ////
+
     /** The interface used for resolving SystemUI SystemProperties Flags to booleans. */
     public interface FlagResolver {
         /** Is the flag enabled? */
@@ -75,33 +107,6 @@
         return MAIN_RESOLVER;
     }
 
-    /** The teamfood flag allows multiple features to be opted into at once. */
-    public static final Flag TEAMFOOD = devFlag("persist.sysui.teamfood");
-
-    /**
-     * Flags related to notification features
-     */
-    public static final class NotificationFlags {
-
-        /**
-         * FOR DEVELOPMENT / TESTING ONLY!!!
-         * Forcibly demote *ALL* FSI notifications as if no apps have the app op permission.
-         */
-        public static final Flag FSI_FORCE_DEMOTE =
-                devFlag("persist.sysui.notification.fsi_force_demote");
-
-        /** Gating the ability for users to dismiss ongoing event notifications */
-        public static final Flag ALLOW_DISMISS_ONGOING =
-                devFlag("persist.sysui.notification.ongoing_dismissal");
-
-        /** Gating the redaction of OTP notifications on the lockscreen */
-        public static final Flag OTP_REDACTION =
-                devFlag("persist.sysui.notification.otp_redaction");
-
-    }
-
-    //// == Everything below this line is the implementation == ////
-
     /**
      * Creates a flag that is enabled by default in debuggable builds.
      * It can be enabled by setting this flag's SystemProperty to 1.
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index fb38bba..bb69192f 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -379,8 +379,12 @@
             // window, as we'll be skipping the addView in handleResumeActivity(), and
             // the token will not be updated as for a new window.
             getAttributes().token = preservedWindow.getAttributes().token;
-            mProxyOnBackInvokedDispatcher.setActualDispatcher(
-                    preservedWindow.getOnBackInvokedDispatcher());
+            final ViewRootImpl viewRoot = mDecor.getViewRootImpl();
+            if (viewRoot != null) {
+                // Clear the old callbacks and attach to the new window.
+                viewRoot.getOnBackInvokedDispatcher().clear();
+                onViewRootImplSet(viewRoot);
+            }
         }
         // Even though the device doesn't support picture-in-picture mode,
         // an user can force using it through developer options.
diff --git a/data/etc/com.android.systemui.xml b/data/etc/com.android.systemui.xml
index e0e13f5..6dcee6d 100644
--- a/data/etc/com.android.systemui.xml
+++ b/data/etc/com.android.systemui.xml
@@ -49,6 +49,7 @@
         <permission name="android.permission.READ_FRAME_BUFFER"/>
         <permission name="android.permission.READ_NETWORK_USAGE_HISTORY"/>
         <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>
+        <permission name="android.permission.READ_PRECISE_PHONE_STATE"/>
         <permission name="android.permission.REAL_GET_TASKS"/>
         <permission name="android.permission.REQUEST_NETWORK_SCORES"/>
         <permission name="android.permission.RECEIVE_MEDIA_RESOURCE_USAGE"/>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
index d276002..88525aa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/ProtoLogController.java
@@ -84,6 +84,15 @@
                 String[] groups = Arrays.copyOfRange(args, 1, args.length);
                 return mShellProtoLog.stopTextLogging(groups, pw) == 0;
             }
+            case "save-for-bugreport": {
+                if (!mShellProtoLog.isProtoEnabled()) {
+                    pw.println("Logging to proto is not enabled for WMShell.");
+                    return false;
+                }
+                mShellProtoLog.stopProtoLog(pw, true /* writeToFile */);
+                mShellProtoLog.startProtoLog(pw);
+                return true;
+            }
             default: {
                 pw.println("Invalid command: " + args[0]);
                 printShellCommandHelp(pw, "");
@@ -108,5 +117,7 @@
         pw.println(prefix + "  Enable logcat logging for given groups.");
         pw.println(prefix + "disable-text [group...]");
         pw.println(prefix + "  Disable logcat logging for given groups.");
+        pw.println(prefix + "save-for-bugreport");
+        pw.println(prefix + "  Flush proto logging to file, only if it's enabled.");
     }
 }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
index 94db878..b0756a0 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvPipModule.java
@@ -206,6 +206,7 @@
             TvPipMenuController tvPipMenuController,
             SyncTransactionQueue syncTransactionQueue,
             TvPipBoundsState tvPipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipTransitionState pipTransitionState,
             TvPipBoundsAlgorithm tvPipBoundsAlgorithm,
             PipAnimationController pipAnimationController,
@@ -217,10 +218,11 @@
             PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer,
             @ShellMainThread ShellExecutor mainExecutor) {
         return new TvPipTaskOrganizer(context,
-                syncTransactionQueue, pipTransitionState, tvPipBoundsState, tvPipBoundsAlgorithm,
-                tvPipMenuController, pipAnimationController, pipSurfaceTransactionHelper,
-                pipTransitionController, pipParamsChangedForwarder, splitScreenControllerOptional,
-                displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor);
+                syncTransactionQueue, pipTransitionState, tvPipBoundsState, pipSizeSpecHandler,
+                tvPipBoundsAlgorithm, tvPipMenuController, pipAnimationController,
+                pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
+                splitScreenControllerOptional, displayController, pipUiEventLogger,
+                shellTaskOrganizer, mainExecutor);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index 1135aa3..efc7d1f 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -442,6 +442,7 @@
             SyncTransactionQueue syncTransactionQueue,
             PipTransitionState pipTransitionState,
             PipBoundsState pipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipBoundsAlgorithm pipBoundsAlgorithm,
             PhonePipMenuController menuPhoneController,
             PipAnimationController pipAnimationController,
@@ -453,10 +454,11 @@
             PipUiEventLogger pipUiEventLogger, ShellTaskOrganizer shellTaskOrganizer,
             @ShellMainThread ShellExecutor mainExecutor) {
         return new PipTaskOrganizer(context,
-                syncTransactionQueue, pipTransitionState, pipBoundsState, pipBoundsAlgorithm,
-                menuPhoneController, pipAnimationController, pipSurfaceTransactionHelper,
-                pipTransitionController, pipParamsChangedForwarder, splitScreenControllerOptional,
-                displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor);
+                syncTransactionQueue, pipTransitionState, pipBoundsState, pipSizeSpecHandler,
+                pipBoundsAlgorithm, menuPhoneController, pipAnimationController,
+                pipSurfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
+                splitScreenControllerOptional, displayController, pipUiEventLogger,
+                shellTaskOrganizer, mainExecutor);
     }
 
     @WMSingleton
@@ -471,13 +473,14 @@
     static PipTransitionController providePipTransitionController(Context context,
             ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, Transitions transitions,
             PipAnimationController pipAnimationController, PipBoundsAlgorithm pipBoundsAlgorithm,
-            PipBoundsState pipBoundsState, PipTransitionState pipTransitionState,
-            PhonePipMenuController pipMenuController,
+            PipBoundsState pipBoundsState, PipSizeSpecHandler pipSizeSpecHandler,
+            PipTransitionState pipTransitionState, PhonePipMenuController pipMenuController,
             PipSurfaceTransactionHelper pipSurfaceTransactionHelper,
             Optional<SplitScreenController> splitScreenOptional) {
         return new PipTransition(context, shellInit, shellTaskOrganizer, transitions,
-                pipBoundsState, pipTransitionState, pipMenuController, pipBoundsAlgorithm,
-                pipAnimationController, pipSurfaceTransactionHelper, splitScreenOptional);
+                pipBoundsState, pipSizeSpecHandler, pipTransitionState, pipMenuController,
+                pipBoundsAlgorithm, pipAnimationController, pipSurfaceTransactionHelper,
+                splitScreenOptional);
     }
 
     @WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
index e91987d..ac13f96 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/kidsmode/KidsModeTaskOrganizer.java
@@ -16,6 +16,7 @@
 
 package com.android.wm.shell.kidsmode;
 
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
 import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
@@ -68,7 +69,7 @@
     private static final String TAG = "KidsModeTaskOrganizer";
 
     private static final int[] CONTROLLED_ACTIVITY_TYPES =
-            {ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD};
+            {ACTIVITY_TYPE_UNDEFINED, ACTIVITY_TYPE_STANDARD, ACTIVITY_TYPE_HOME};
     private static final int[] CONTROLLED_WINDOWING_MODES =
             {WINDOWING_MODE_FULLSCREEN, WINDOWING_MODE_UNDEFINED};
 
@@ -93,6 +94,8 @@
     private KidsModeSettingsObserver mKidsModeSettingsObserver;
     private boolean mEnabled;
 
+    private ActivityManager.RunningTaskInfo mHomeTask;
+
     private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
@@ -219,6 +222,13 @@
         }
         super.onTaskAppeared(taskInfo, leash);
 
+        // Only allow home to draw under system bars.
+        if (taskInfo.getActivityType() == ACTIVITY_TYPE_HOME) {
+            final WindowContainerTransaction wct = getWindowContainerTransaction();
+            wct.setBounds(taskInfo.token, new Rect(0, 0, mDisplayWidth, mDisplayHeight));
+            mSyncQueue.queue(wct);
+            mHomeTask = taskInfo;
+        }
         mSyncQueue.runInSync(t -> {
             // Reset several properties back to fullscreen (PiP, for example, leaves all these
             // properties in a bad state).
@@ -291,6 +301,13 @@
         }
         mLaunchRootTask = null;
         mLaunchRootLeash = null;
+        if (mHomeTask != null && mHomeTask.token != null) {
+            final WindowContainerToken homeToken = mHomeTask.token;
+            final WindowContainerTransaction wct = getWindowContainerTransaction();
+            wct.setBounds(homeToken, null);
+            mSyncQueue.queue(wct);
+        }
+        mHomeTask = null;
         unregisterOrganizer();
     }
 
@@ -320,7 +337,7 @@
             final SurfaceControl rootLeash = mLaunchRootLeash;
             mSyncQueue.runInSync(t -> {
                 t.setPosition(rootLeash, taskBounds.left, taskBounds.top);
-                t.setWindowCrop(rootLeash, taskBounds.width(), taskBounds.height());
+                t.setWindowCrop(rootLeash, mDisplayWidth, mDisplayHeight);
             });
         }
     }
@@ -351,7 +368,7 @@
         final SurfaceControl finalLeash = mLaunchRootLeash;
         mSyncQueue.runInSync(t -> {
             t.setPosition(finalLeash, taskBounds.left, taskBounds.top);
-            t.setWindowCrop(finalLeash, taskBounds.width(), taskBounds.height());
+            t.setWindowCrop(finalLeash, mDisplayWidth, mDisplayHeight);
         });
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
index 61da10b..5be18d8 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipBoundsState.java
@@ -311,10 +311,10 @@
         mDisplayLayout.set(displayLayout);
     }
 
-    /** Get the display layout. */
+    /** Get a copy of the display layout. */
     @NonNull
     public DisplayLayout getDisplayLayout() {
-        return mDisplayLayout;
+        return new DisplayLayout(mDisplayLayout);
     }
 
     @VisibleForTesting
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index aad27b9..ffb2893 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -79,11 +79,13 @@
 import com.android.wm.shell.ShellTaskOrganizer;
 import com.android.wm.shell.animation.Interpolators;
 import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayLayout;
 import com.android.wm.shell.common.ScreenshotUtils;
 import com.android.wm.shell.common.ShellExecutor;
 import com.android.wm.shell.common.SyncTransactionQueue;
 import com.android.wm.shell.common.annotations.ShellMainThread;
 import com.android.wm.shell.pip.phone.PipMotionHelper;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.transition.Transitions;
@@ -126,6 +128,7 @@
     private final Context mContext;
     private final SyncTransactionQueue mSyncTransactionQueue;
     private final PipBoundsState mPipBoundsState;
+    private final PipSizeSpecHandler mPipSizeSpecHandler;
     private final PipBoundsAlgorithm mPipBoundsAlgorithm;
     private final @NonNull PipMenuController mPipMenuController;
     private final PipAnimationController mPipAnimationController;
@@ -313,6 +316,7 @@
             @NonNull SyncTransactionQueue syncTransactionQueue,
             @NonNull PipTransitionState pipTransitionState,
             @NonNull PipBoundsState pipBoundsState,
+            @NonNull PipSizeSpecHandler pipSizeSpecHandler,
             @NonNull PipBoundsAlgorithm boundsHandler,
             @NonNull PipMenuController pipMenuController,
             @NonNull PipAnimationController pipAnimationController,
@@ -328,6 +332,7 @@
         mSyncTransactionQueue = syncTransactionQueue;
         mPipTransitionState = pipTransitionState;
         mPipBoundsState = pipBoundsState;
+        mPipSizeSpecHandler = pipSizeSpecHandler;
         mPipBoundsAlgorithm = boundsHandler;
         mPipMenuController = pipMenuController;
         mPipTransitionController = pipTransitionController;
@@ -1601,7 +1606,12 @@
     private @Nullable Rect computeRotatedBounds(int rotationDelta, int direction,
             Rect outDestinationBounds, Rect sourceHintRect) {
         if (direction == TRANSITION_DIRECTION_TO_PIP) {
-            mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), mNextRotation);
+            DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
+
+            layoutCopy.rotateTo(mContext.getResources(), mNextRotation);
+            mPipBoundsState.setDisplayLayout(layoutCopy);
+            mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
+
             final Rect displayBounds = mPipBoundsState.getDisplayBounds();
             outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds());
             // Transform the destination bounds to current display coordinates.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
index d9d1009..e5c0570 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java
@@ -65,6 +65,8 @@
 import com.android.internal.protolog.common.ProtoLog;
 import com.android.wm.shell.R;
 import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayLayout;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.protolog.ShellProtoLogGroup;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 import com.android.wm.shell.sysui.ShellInit;
@@ -83,6 +85,7 @@
 
     private final Context mContext;
     private final PipTransitionState mPipTransitionState;
+    private final PipSizeSpecHandler mPipSizeSpecHandler;
     private final int mEnterExitAnimationDuration;
     private final PipSurfaceTransactionHelper mSurfaceTransactionHelper;
     private final Optional<SplitScreenController> mSplitScreenOptional;
@@ -113,6 +116,7 @@
             @NonNull ShellTaskOrganizer shellTaskOrganizer,
             @NonNull Transitions transitions,
             PipBoundsState pipBoundsState,
+            PipSizeSpecHandler pipSizeSpecHandler,
             PipTransitionState pipTransitionState,
             PipMenuController pipMenuController,
             PipBoundsAlgorithm pipBoundsAlgorithm,
@@ -123,6 +127,7 @@
                 pipBoundsAlgorithm, pipAnimationController);
         mContext = context;
         mPipTransitionState = pipTransitionState;
+        mPipSizeSpecHandler = pipSizeSpecHandler;
         mEnterExitAnimationDuration = context.getResources()
                 .getInteger(R.integer.config_pipResizeAnimationDuration);
         mSurfaceTransactionHelper = pipSurfaceTransactionHelper;
@@ -308,7 +313,12 @@
             // initial state under the new rotation.
             int rotationDelta = deltaRotation(startRotation, endRotation);
             if (rotationDelta != Surface.ROTATION_0) {
-                mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), endRotation);
+                DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
+
+                layoutCopy.rotateTo(mContext.getResources(), endRotation);
+                mPipBoundsState.setDisplayLayout(layoutCopy);
+                mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
+
                 final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
                 wct.setBounds(mRequestedEnterTask, destinationBounds);
                 return true;
@@ -824,7 +834,12 @@
     /** Computes destination bounds in old rotation and updates source hint rect if available. */
     private void computeEnterPipRotatedBounds(int rotationDelta, int startRotation, int endRotation,
             TaskInfo taskInfo, Rect outDestinationBounds, @Nullable Rect outSourceHintRect) {
-        mPipBoundsState.getDisplayLayout().rotateTo(mContext.getResources(), endRotation);
+        DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
+
+        layoutCopy.rotateTo(mContext.getResources(), endRotation);
+        mPipBoundsState.setDisplayLayout(layoutCopy);
+        mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
+
         final Rect displayBounds = mPipBoundsState.getDisplayBounds();
         outDestinationBounds.set(mPipBoundsAlgorithm.getEntryDestinationBounds());
         // Transform the destination bounds to current display coordinates.
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 d86468a..4958b25 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
@@ -1032,7 +1032,11 @@
     private void onDisplayRotationChangedNotInPip(Context context, int toRotation) {
         // Update the display layout, note that we have to do this on every rotation even if we
         // aren't in PIP since we need to update the display layout to get the right resources
-        mPipBoundsState.getDisplayLayout().rotateTo(context.getResources(), toRotation);
+        DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
+
+        layoutCopy.rotateTo(context.getResources(), toRotation);
+        mPipBoundsState.setDisplayLayout(layoutCopy);
+        mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
     }
 
     /**
@@ -1069,7 +1073,11 @@
                         mPipBoundsState.getStashedState());
 
         // Update the display layout
-        mPipBoundsState.getDisplayLayout().rotateTo(context.getResources(), toRotation);
+        DisplayLayout layoutCopy = mPipBoundsState.getDisplayLayout();
+
+        layoutCopy.rotateTo(context.getResources(), toRotation);
+        mPipBoundsState.setDisplayLayout(layoutCopy);
+        mPipSizeSpecHandler.setDisplayLayout(layoutCopy);
 
         // Calculate the stack bounds in the new orientation based on same fraction along the
         // rotated movement bounds.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
index e787ed9..d03d075 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipSizeSpecHandler.java
@@ -409,12 +409,6 @@
         return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height());
     }
 
-    /** Get the display layout. */
-    @NonNull
-    public DisplayLayout getDisplayLayout() {
-        return mDisplayLayout;
-    }
-
     /** Update the display layout. */
     public void setDisplayLayout(@NonNull DisplayLayout displayLayout) {
         mDisplayLayout.set(displayLayout);
@@ -429,12 +423,11 @@
      */
     public Rect getInsetBounds() {
         Rect insetBounds = new Rect();
-        final DisplayLayout displayLayout = getDisplayLayout();
-        Rect insets = getDisplayLayout().stableInsets();
+        Rect insets = mDisplayLayout.stableInsets();
         insetBounds.set(insets.left + mScreenEdgeInsets.x,
                 insets.top + mScreenEdgeInsets.y,
-                displayLayout.width() - insets.right - mScreenEdgeInsets.x,
-                displayLayout.height() - insets.bottom - mScreenEdgeInsets.y);
+                mDisplayLayout.width() - insets.right - mScreenEdgeInsets.x,
+                mDisplayLayout.height() - insets.bottom - mScreenEdgeInsets.y);
         return insetBounds;
     }
 
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
index 42fd1aa..be9b936 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java
@@ -36,6 +36,7 @@
 import com.android.wm.shell.pip.PipTransitionState;
 import com.android.wm.shell.pip.PipUiEventLogger;
 import com.android.wm.shell.pip.PipUtils;
+import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
 import com.android.wm.shell.splitscreen.SplitScreenController;
 
 import java.util.Objects;
@@ -50,6 +51,7 @@
             @NonNull SyncTransactionQueue syncTransactionQueue,
             @NonNull PipTransitionState pipTransitionState,
             @NonNull PipBoundsState pipBoundsState,
+            @NonNull PipSizeSpecHandler pipSizeSpecHandler,
             @NonNull PipBoundsAlgorithm boundsHandler,
             @NonNull PipMenuController pipMenuController,
             @NonNull PipAnimationController pipAnimationController,
@@ -61,10 +63,11 @@
             @NonNull PipUiEventLogger pipUiEventLogger,
             @NonNull ShellTaskOrganizer shellTaskOrganizer,
             ShellExecutor mainExecutor) {
-        super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, boundsHandler,
-                pipMenuController, pipAnimationController, surfaceTransactionHelper,
-                pipTransitionController, pipParamsChangedForwarder, splitScreenOptional,
-                displayController, pipUiEventLogger, shellTaskOrganizer, mainExecutor);
+        super(context, syncTransactionQueue, pipTransitionState, pipBoundsState, pipSizeSpecHandler,
+                boundsHandler, pipMenuController, pipAnimationController,
+                surfaceTransactionHelper, pipTransitionController, pipParamsChangedForwarder,
+                splitScreenOptional, displayController, pipUiEventLogger, shellTaskOrganizer,
+                mainExecutor);
     }
 
     @Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
index 56aa742..81e118a 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl
@@ -122,9 +122,9 @@
      * Start a pair of intents using legacy transition system.
      */
     oneway void startIntentsWithLegacyTransition(in PendingIntent pendingIntent1,
-            in Bundle options1, in PendingIntent pendingIntent2, in Bundle options2,
-            int splitPosition, float splitRatio, in RemoteAnimationAdapter adapter,
-            in InstanceId instanceId) = 18;
+            in ShortcutInfo shortcutInfo1, in Bundle options1, in PendingIntent pendingIntent2,
+            in ShortcutInfo shortcutInfo2, in Bundle options2, int splitPosition, float splitRatio,
+            in RemoteAnimationAdapter adapter, in InstanceId instanceId) = 18;
 
     /**
      * Start a pair of intents in one transition.
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
index 7cb5cf2..36da4cf 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java
@@ -583,9 +583,10 @@
     }
 
     private void startIntentsWithLegacyTransition(PendingIntent pendingIntent1,
-            @Nullable Bundle options1, PendingIntent pendingIntent2,
-            @Nullable Bundle options2, @SplitPosition int splitPosition,
-            float splitRatio, RemoteAnimationAdapter adapter, InstanceId instanceId) {
+            @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
+            PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2,
+            @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
+            RemoteAnimationAdapter adapter, InstanceId instanceId) {
         Intent fillInIntent1 = null;
         Intent fillInIntent2 = null;
         final String packageName1 = SplitScreenUtils.getPackageName(pendingIntent1);
@@ -605,9 +606,9 @@
                         Toast.LENGTH_SHORT).show();
             }
         }
-        mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1, options1,
-                pendingIntent2, fillInIntent2, options2, splitPosition, splitRatio, adapter,
-                instanceId);
+        mStageCoordinator.startIntentsWithLegacyTransition(pendingIntent1, fillInIntent1,
+                shortcutInfo1, options1, pendingIntent2, fillInIntent2, shortcutInfo2, options2,
+                splitPosition, splitRatio, adapter, instanceId);
     }
 
     @Override
@@ -1037,13 +1038,14 @@
 
         @Override
         public void startIntentsWithLegacyTransition(PendingIntent pendingIntent1,
-                @Nullable Bundle options1, PendingIntent pendingIntent2, @Nullable Bundle options2,
-                @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
-                InstanceId instanceId) {
+                @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
+                PendingIntent pendingIntent2, @Nullable ShortcutInfo shortcutInfo2,
+                @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
+                RemoteAnimationAdapter adapter, InstanceId instanceId) {
             executeRemoteCallWithTaskPermission(mController, "startIntentsWithLegacyTransition",
                     (controller) ->
-                        controller.startIntentsWithLegacyTransition(
-                                pendingIntent1, options1, pendingIntent2, options2, splitPosition,
+                        controller.startIntentsWithLegacyTransition(pendingIntent1, shortcutInfo1,
+                                options1, pendingIntent2, shortcutInfo2, options2, splitPosition,
                                 splitRatio, adapter, instanceId)
                     );
         }
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 219f87e..0f18dda 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -692,9 +692,11 @@
 
     /** Starts a pair of intents using legacy transition. */
     void startIntentsWithLegacyTransition(PendingIntent pendingIntent1, Intent fillInIntent1,
-            @Nullable Bundle options1, PendingIntent pendingIntent2, Intent fillInIntent2,
-            @Nullable Bundle options2, @SplitPosition int splitPosition, float splitRatio,
-            RemoteAnimationAdapter adapter, InstanceId instanceId) {
+            @Nullable ShortcutInfo shortcutInfo1, @Nullable Bundle options1,
+            @Nullable PendingIntent pendingIntent2, Intent fillInIntent2,
+            @Nullable ShortcutInfo shortcutInfo2, @Nullable Bundle options2,
+            @SplitPosition int splitPosition, float splitRatio, RemoteAnimationAdapter adapter,
+            InstanceId instanceId) {
         final WindowContainerTransaction wct = new WindowContainerTransaction();
         if (options1 == null) options1 = new Bundle();
         if (pendingIntent2 == null) {
@@ -703,15 +705,23 @@
             activityOptions.update(ActivityOptions.makeRemoteAnimation(adapter));
             options1 = activityOptions.toBundle();
             addActivityOptions(options1, null /* launchTarget */);
-            wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+            if (shortcutInfo1 != null) {
+                wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1);
+            } else {
+                wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+            }
             mSyncQueue.queue(wct);
             return;
         }
 
         addActivityOptions(options1, mSideStage);
-        wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
-        startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, options2, splitPosition,
-                splitRatio, adapter, instanceId);
+        if (shortcutInfo1 != null) {
+            wct.startShortcut(mContext.getPackageName(), shortcutInfo1, options1);
+        } else {
+            wct.sendPendingIntent(pendingIntent1, fillInIntent1, options1);
+        }
+        startWithLegacyTransition(wct, pendingIntent2, fillInIntent2, shortcutInfo2, options2,
+                splitPosition, splitRatio, adapter, instanceId);
     }
 
     void startIntentAndTaskWithLegacyTransition(PendingIntent pendingIntent, Intent fillInIntent,
@@ -763,18 +773,19 @@
 
     private void startWithLegacyTransition(WindowContainerTransaction wct,
             @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent,
-            @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio,
-            RemoteAnimationAdapter adapter, InstanceId instanceId) {
+            @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle mainOptions,
+            @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+            InstanceId instanceId) {
         startWithLegacyTransition(wct, INVALID_TASK_ID, mainPendingIntent, mainFillInIntent,
-                mainOptions, sidePosition, splitRatio, adapter, instanceId);
+                mainShortcutInfo, mainOptions, sidePosition, splitRatio, adapter, instanceId);
     }
 
     private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId,
             @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio,
             RemoteAnimationAdapter adapter, InstanceId instanceId) {
         startWithLegacyTransition(wct, mainTaskId, null /* mainPendingIntent */,
-                null /* mainFillInIntent */, mainOptions, sidePosition, splitRatio, adapter,
-                instanceId);
+                null /* mainFillInIntent */, null /* mainShortcutInfo */, mainOptions, sidePosition,
+                splitRatio, adapter, instanceId);
     }
 
     /**
@@ -784,8 +795,9 @@
      */
     private void startWithLegacyTransition(WindowContainerTransaction wct, int mainTaskId,
             @Nullable PendingIntent mainPendingIntent, @Nullable Intent mainFillInIntent,
-            @Nullable Bundle mainOptions, @SplitPosition int sidePosition, float splitRatio,
-            RemoteAnimationAdapter adapter, InstanceId instanceId) {
+            @Nullable ShortcutInfo mainShortcutInfo, @Nullable Bundle options,
+            @SplitPosition int sidePosition, float splitRatio, RemoteAnimationAdapter adapter,
+            InstanceId instanceId) {
         if (!isSplitScreenVisible()) {
             exitSplitScreen(null /* childrenToTop */, EXIT_REASON_RECREATE_SPLIT);
         }
@@ -809,15 +821,19 @@
             mMainStage.activate(wct, false /* reparent */);
         }
 
-        if (mainOptions == null) mainOptions = new Bundle();
-        addActivityOptions(mainOptions, mMainStage);
-        mainOptions = wrapAsSplitRemoteAnimation(adapter, mainOptions);
+        if (options == null) options = new Bundle();
+        addActivityOptions(options, mMainStage);
+        options = wrapAsSplitRemoteAnimation(adapter, options);
 
         updateWindowBounds(mSplitLayout, wct);
-        if (mainTaskId == INVALID_TASK_ID) {
-            wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, mainOptions);
+
+        // TODO(b/268008375): Merge APIs to start a split pair into one.
+        if (mainTaskId != INVALID_TASK_ID) {
+            wct.startTask(mainTaskId, options);
+        } else if (mainShortcutInfo != null) {
+            wct.startShortcut(mContext.getPackageName(), mainShortcutInfo, options);
         } else {
-            wct.startTask(mainTaskId, mainOptions);
+            wct.sendPendingIntent(mainPendingIntent, mainFillInIntent, options);
         }
 
         wct.reorder(mRootTaskInfo.token, true);
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
index 2da4af8..e907cd3 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java
@@ -104,8 +104,8 @@
                 new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {},
                 mPipSizeSpecHandler);
         mMainExecutor = new TestShellExecutor();
-        mPipTaskOrganizer = new PipTaskOrganizer(mContext,
-                mMockSyncTransactionQueue, mPipTransitionState, mPipBoundsState,
+        mPipTaskOrganizer = new PipTaskOrganizer(mContext, mMockSyncTransactionQueue,
+                mPipTransitionState, mPipBoundsState, mPipSizeSpecHandler,
                 mPipBoundsAlgorithm, mMockPhonePipMenuController, mMockPipAnimationController,
                 mMockPipSurfaceTransactionHelper, mMockPipTransitionController,
                 mMockPipParamsChangedForwarder, mMockOptionalSplitScreen, mMockDisplayController,
diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java
index ded9597..a5757b9 100644
--- a/media/java/android/media/AudioAttributes.java
+++ b/media/java/android/media/AudioAttributes.java
@@ -1275,7 +1275,10 @@
                     || (preset == MediaRecorder.AudioSource.VOICE_UPLINK)
                     || (preset == MediaRecorder.AudioSource.VOICE_CALL)
                     || (preset == MediaRecorder.AudioSource.ECHO_REFERENCE)
-                    || (preset == MediaRecorder.AudioSource.ULTRASOUND)) {
+                    || (preset == MediaRecorder.AudioSource.ULTRASOUND)
+                    // AUDIO_SOURCE_INVALID is used by convention on default initialized
+                    // audio attributes
+                    || (preset == MediaRecorder.AudioSource.AUDIO_SOURCE_INVALID)) {
                 mSource = preset;
             } else {
                 setCapturePreset(preset);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 2529157..ed62c5f 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -204,6 +204,45 @@
     path: "tests/utils/src",
 }
 
+filegroup {
+    name: "SystemUI-tests-robolectric-pilots",
+    srcs: [
+        // data
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/FakeKeyguardQuickAffordanceConfig.kt",
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/quickaffordance/QuickAccessWalletKeyguardQuickAffordanceConfigTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryTest.kt",
+        "tests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt",
+        // domain
+        "tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt",
+        "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt",
+        "tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt",
+        "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt",
+        "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt",
+        "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt",
+        "tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt",
+        "tests/src/com/android/systemui/keyguard/domain/quickaffordance/FakeKeyguardQuickAffordanceRegistry.kt",
+        "tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt",
+        "tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt",
+        "tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt",
+        // ui
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt",
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt",
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt",
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt",
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt",
+        "tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt",
+    ],
+    path: "tests/src",
+}
+
 java_library {
     name: "SystemUI-tests-concurrency",
     srcs: [
@@ -315,8 +354,16 @@
     defaults: [
         "platform_app_defaults",
         "SystemUI_app_defaults",
+        "SystemUI_compose_defaults",
     ],
     manifest: "tests/AndroidManifest-base.xml",
+
+    srcs: [
+        "src/**/*.kt",
+        "src/**/*.java",
+        "src/**/I*.aidl",
+        ":ReleaseJavaFiles",
+    ],
     static_libs: [
         "SystemUI-tests-base",
     ],
@@ -330,6 +377,9 @@
     certificate: "platform",
     privileged: true,
     resource_dirs: [],
+    kotlincflags: ["-Xjvm-default=all"],
+
+    plugins: ["dagger2-compiler"],
 }
 
 android_robolectric_test {
@@ -337,6 +387,13 @@
     srcs: [
         "tests/robolectric/src/**/*.kt",
         "tests/robolectric/src/**/*.java",
+        ":SystemUI-tests-utils",
+        ":SystemUI-tests-robolectric-pilots",
+    ],
+    static_libs: [
+        "androidx.test.uiautomator_uiautomator",
+        "androidx.test.ext.junit",
+        "inline-mockito-robolectric-prebuilt",
     ],
     libs: [
         "android.test.runner",
@@ -344,7 +401,9 @@
         "android.test.mock",
         "truth-prebuilt",
     ],
-    kotlincflags: ["-Xjvm-default=enable"],
+
+    upstream: true,
+
     instrumentation_for: "SystemUIRobo-stub",
     java_resource_dirs: ["tests/robolectric/config"],
 }
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index e96aead5..71a82bf 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -68,6 +68,7 @@
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
     <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
     <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
+    <uses-permission android:name="android.permission.READ_PRECISE_PHONE_STATE" />
     <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
     <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
     <uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG" />
@@ -899,7 +900,7 @@
                   android:showWhenLocked="true"
                   android:showForAllUsers="true"
                   android:finishOnTaskLaunch="true"
-                  android:launchMode="singleInstance"
+                  android:lockTaskMode="if_whitelisted"
                   android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboard|keyboardHidden"
                   android:visibleToInstantApps="true">
         </activity>
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
index 7897934..442c6fa 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimation.kt
@@ -66,11 +66,28 @@
     fun isPlaying(): Boolean = animator.isRunning
 
     private fun applyConfigToShader() {
-        rippleShader.setCenter(config.centerX, config.centerY)
-        rippleShader.setMaxSize(config.maxWidth, config.maxHeight)
-        rippleShader.rippleFill = config.shouldFillRipple
-        rippleShader.pixelDensity = config.pixelDensity
-        rippleShader.color = ColorUtils.setAlphaComponent(config.color, config.opacity)
-        rippleShader.sparkleStrength = config.sparkleStrength
+        with(rippleShader) {
+            setCenter(config.centerX, config.centerY)
+            setMaxSize(config.maxWidth, config.maxHeight)
+            pixelDensity = config.pixelDensity
+            color = ColorUtils.setAlphaComponent(config.color, config.opacity)
+            sparkleStrength = config.sparkleStrength
+
+            assignFadeParams(baseRingFadeParams, config.baseRingFadeParams)
+            assignFadeParams(sparkleRingFadeParams, config.sparkleRingFadeParams)
+            assignFadeParams(centerFillFadeParams, config.centerFillFadeParams)
+        }
+    }
+
+    private fun assignFadeParams(
+        destFadeParams: RippleShader.FadeParams,
+        srcFadeParams: RippleShader.FadeParams?
+    ) {
+        srcFadeParams?.let {
+            destFadeParams.fadeInStart = it.fadeInStart
+            destFadeParams.fadeInEnd = it.fadeInEnd
+            destFadeParams.fadeOutStart = it.fadeOutStart
+            destFadeParams.fadeOutEnd = it.fadeOutEnd
+        }
     }
 }
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
index 773ac55..1786d13 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationConfig.kt
@@ -20,8 +20,11 @@
     val pixelDensity: Float = 1f,
     var color: Int = Color.WHITE,
     val opacity: Int = RIPPLE_DEFAULT_ALPHA,
-    val shouldFillRipple: Boolean = false,
     val sparkleStrength: Float = RIPPLE_SPARKLE_STRENGTH,
+    // Null means it uses default fade parameter values.
+    val baseRingFadeParams: RippleShader.FadeParams? = null,
+    val sparkleRingFadeParams: RippleShader.FadeParams? = null,
+    val centerFillFadeParams: RippleShader.FadeParams? = null,
     val shouldDistort: Boolean = true
 ) {
     companion object {
diff --git a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
index 74bc910..61ca90a 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/surfaceeffects/ripple/RippleShader.kt
@@ -82,7 +82,7 @@
                 vec2 p_distorted = distort(p, in_time, in_distort_radial, in_distort_xy);
                 float radius = in_size.x * 0.5;
                 float sparkleRing = soften(circleRing(p_distorted-in_center, radius), in_blur);
-                float inside = soften(sdCircle(p_distorted-in_center, radius * 1.2), in_blur);
+                float inside = soften(sdCircle(p_distorted-in_center, radius * 1.25), in_blur);
                 float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_time * 0.00175)
                     * (1.-sparkleRing) * in_fadeSparkle;
 
@@ -270,38 +270,6 @@
     var currentHeight: Float = 0f
         private set
 
-    /**
-     * True if the ripple should stayed filled in as it expands to give a filled-in circle effect.
-     * False for a ring effect.
-     *
-     * <p>You must reset fade params after changing this.
-     *
-     * TODO(b/265326983): Remove this and only expose fade params.
-     */
-    var rippleFill: Boolean = false
-        set(value) {
-            if (value) {
-                baseRingFadeParams.fadeOutStart = 1f
-                baseRingFadeParams.fadeOutEnd = 1f
-
-                centerFillFadeParams.fadeInStart = 0f
-                centerFillFadeParams.fadeInEnd = 0f
-                centerFillFadeParams.fadeOutStart = 1f
-                centerFillFadeParams.fadeOutEnd = 1f
-            } else {
-                // Set back to the original fade parameters.
-                // Ideally this should be set by the client as they know the initial value.
-                baseRingFadeParams.fadeOutStart = DEFAULT_BASE_RING_FADE_OUT_START
-                baseRingFadeParams.fadeOutEnd = DEFAULT_FADE_OUT_END
-
-                centerFillFadeParams.fadeInStart = DEFAULT_FADE_IN_START
-                centerFillFadeParams.fadeInEnd = DEFAULT_CENTER_FILL_FADE_IN_END
-                centerFillFadeParams.fadeOutStart = DEFAULT_CENTER_FILL_FADE_OUT_START
-                centerFillFadeParams.fadeOutEnd = DEFAULT_CENTER_FILL_FADE_OUT_END
-            }
-            field = value
-        }
-
     /** Parameters that are used to fade in/ out of the sparkle ring. */
     val sparkleRingFadeParams =
         FadeParams(
@@ -324,12 +292,7 @@
             DEFAULT_FADE_OUT_END
         )
 
-    /**
-     * Parameters that are used to fade in/ out of the center fill.
-     *
-     * <p>Note that if [rippleFill] is set to true, those will be ignored and the center fill will
-     * be always full alpha.
-     */
+    /** Parameters that are used to fade in/ out of the center fill. */
     val centerFillFadeParams =
         FadeParams(
             DEFAULT_FADE_IN_START,
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
index 4ef525a..590015d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/ClockProviderPlugin.kt
@@ -20,7 +20,6 @@
 import com.android.internal.annotations.Keep
 import com.android.systemui.plugins.annotations.ProvidesInterface
 import com.android.systemui.plugins.log.LogBuffer
-import com.android.systemui.statusbar.Weather
 import java.io.PrintWriter
 import java.util.Locale
 import java.util.TimeZone
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt
index 85ec42d..302f175 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/Weather.kt
@@ -1,4 +1,4 @@
-package com.android.systemui.statusbar
+package com.android.systemui.plugins
 
 import android.os.Bundle
 
diff --git a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher_item.xml b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher_item.xml
index c388f15..81f4c8c 100644
--- a/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher_item.xml
+++ b/packages/SystemUI/res-keyguard/layout/keyguard_bouncer_user_switcher_item.xml
@@ -15,6 +15,7 @@
   -->
 <FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/user_switcher_item"
     android:layout_width="match_parent"
     android:layout_height="wrap_content">
   <TextView
diff --git a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
index 4483db8..02186fc 100644
--- a/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
+++ b/packages/SystemUI/res/layout/media_ttt_chip_receiver.xml
@@ -27,22 +27,28 @@
         android:layout_height="wrap_content"
         />
 
-    <com.android.systemui.media.taptotransfer.receiver.ReceiverChipRippleView
-        android:id="@+id/icon_glow_ripple"
+    <FrameLayout
+        android:id="@+id/icon_container_view"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        />
-
-    <!-- Add a bottom margin to avoid the glow of the icon ripple from being cropped by screen
-     bounds while animating with the icon -->
-    <com.android.internal.widget.CachingIconView
-        android:id="@+id/app_icon"
-        android:background="@drawable/media_ttt_chip_background_receiver"
-        android:layout_width="@dimen/media_ttt_icon_size_receiver"
-        android:layout_height="@dimen/media_ttt_icon_size_receiver"
-        android:layout_gravity="center|bottom"
         android:alpha="0.0"
-        android:layout_marginBottom="@dimen/media_ttt_receiver_icon_bottom_margin"
-        />
+        >
+        <com.android.systemui.media.taptotransfer.receiver.ReceiverChipRippleView
+            android:id="@+id/icon_glow_ripple"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            />
+
+        <!-- Add a bottom margin to avoid the glow of the icon ripple from being cropped by screen
+        bounds while animating with the icon -->
+        <com.android.internal.widget.CachingIconView
+            android:id="@+id/app_icon"
+            android:background="@drawable/media_ttt_chip_background_receiver"
+            android:layout_width="@dimen/media_ttt_icon_size_receiver"
+            android:layout_height="@dimen/media_ttt_icon_size_receiver"
+            android:layout_gravity="center|bottom"
+            android:layout_marginBottom="@dimen/media_ttt_receiver_icon_bottom_margin"
+            />
+    </FrameLayout>
 
 </FrameLayout>
diff --git a/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml b/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml
index 2567176..130472d 100644
--- a/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml
+++ b/packages/SystemUI/res/layout/screen_record_dialog_audio_source.xml
@@ -23,7 +23,7 @@
     <TextView
         android:id="@+id/screen_recording_dialog_source_text"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_height="wrap_content"
         android:layout_gravity="center_vertical"
         android:textAppearance="?android:attr/textAppearanceMedium"
         android:textSize="14sp"
diff --git a/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml b/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml
index e2b8d33..9d9f5c2 100644
--- a/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml
+++ b/packages/SystemUI/res/layout/screen_record_dialog_audio_source_selected.xml
@@ -22,7 +22,7 @@
     android:layout_weight="1">
     <TextView
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_height="wrap_content"
         android:text="@string/screenrecord_audio_label"
         android:textAppearance="?android:attr/textAppearanceMedium"
         android:fontFamily="@*android:string/config_headlineFontFamily"
@@ -30,7 +30,7 @@
     <TextView
         android:id="@+id/screen_recording_dialog_source_text"
         android:layout_width="match_parent"
-        android:layout_height="match_parent"
+        android:layout_height="wrap_content"
         android:textColor="?android:attr/textColorSecondary"
         android:textAppearance="?android:attr/textAppearanceSmall"/>
 </LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/screen_record_options.xml b/packages/SystemUI/res/layout/screen_record_options.xml
index 3f0eea9..6cc72dd 100644
--- a/packages/SystemUI/res/layout/screen_record_options.xml
+++ b/packages/SystemUI/res/layout/screen_record_options.xml
@@ -67,7 +67,7 @@
             android:importantForAccessibility="no"/>
         <TextView
             android:layout_width="0dp"
-            android:layout_height="match_parent"
+            android:layout_height="wrap_content"
             android:minHeight="48dp"
             android:layout_weight="1"
             android:gravity="center_vertical"
diff --git a/packages/SystemUI/res/layout/status_bar_notification_row.xml b/packages/SystemUI/res/layout/status_bar_notification_row.xml
index 2c08f5d..356b36f 100644
--- a/packages/SystemUI/res/layout/status_bar_notification_row.xml
+++ b/packages/SystemUI/res/layout/status_bar_notification_row.xml
@@ -39,8 +39,11 @@
 
     <com.android.systemui.statusbar.notification.row.NotificationContentView
         android:id="@+id/expanded"
-       android:layout_width="match_parent"
-       android:layout_height="wrap_content" />
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:minHeight="@dimen/notification_content_min_height"
+        android:gravity="center_vertical"
+        />
 
     <com.android.systemui.statusbar.notification.row.NotificationContentView
         android:id="@+id/expandedPublic"
diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
index 5d78e4e..2a27b47 100644
--- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml
@@ -45,8 +45,6 @@
     <item name="controls_task_view_width_percentage" translatable="false" format="float" type="dimen">0.45</item>
     <dimen name="controls_task_view_right_margin">8dp</dimen>
 
-    <dimen name="status_bar_header_height_keyguard">42dp</dimen>
-
     <dimen name="lockscreen_shade_max_over_scroll_amount">32dp</dimen>
 
     <dimen name="status_view_margin_horizontal">8dp</dimen>
diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml
index 4f24d83..45b137a 100644
--- a/packages/SystemUI/res/values-sw600dp/dimens.xml
+++ b/packages/SystemUI/res/values-sw600dp/dimens.xml
@@ -16,8 +16,9 @@
 */
 -->
 <resources>
-    <!-- Height of the status bar header bar when on Keyguard -->
-    <dimen name="status_bar_header_height_keyguard">60dp</dimen>
+    <!-- Height of the status bar header bar when on Keyguard.
+         On large screens should be the same as the regular status bar. -->
+    <dimen name="status_bar_header_height_keyguard">@dimen/status_bar_height</dimen>
 
     <!-- Size of user icon + frame in the qs user picker (incl. frame) -->
     <dimen name="qs_framed_avatar_size">60dp</dimen>
diff --git a/packages/SystemUI/res/values-sw720dp-land/dimens.xml b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
index 2b88e55..9ed9360 100644
--- a/packages/SystemUI/res/values-sw720dp-land/dimens.xml
+++ b/packages/SystemUI/res/values-sw720dp-land/dimens.xml
@@ -22,8 +22,6 @@
 
     <dimen name="keyguard_split_shade_top_margin">72dp</dimen>
 
-    <dimen name="status_bar_header_height_keyguard">56dp</dimen>
-
     <dimen name="status_view_margin_horizontal">24dp</dimen>
 
     <dimen name="qs_media_session_height_expanded">184dp</dimen>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index d492e53c..9391714 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -184,6 +184,15 @@
     <!-- Height of a small notification in the status bar-->
     <dimen name="notification_min_height">@*android:dimen/notification_min_height</dimen>
 
+    <!-- Minimum allowed height of notifications -->
+    <dimen name="notification_validation_minimum_allowed_height">10dp</dimen>
+
+    <!-- Minimum height for displaying notification content. -->
+    <dimen name="notification_content_min_height">48dp</dimen>
+
+    <!-- Reference width used when validating notification layouts -->
+    <dimen name="notification_validation_reference_width">320dp</dimen>
+
     <!-- Increased height of a small notification in the status bar -->
     <dimen name="notification_min_height_increased">146dp</dimen>
 
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java
index 95675ce..209d5e8 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/condition/Monitor.java
@@ -38,13 +38,19 @@
 public class Monitor {
     private final String mTag = getClass().getSimpleName();
     private final Executor mExecutor;
+    private final Set<Condition> mPreconditions;
 
     private final HashMap<Condition, ArraySet<Subscription.Token>> mConditions = new HashMap<>();
     private final HashMap<Subscription.Token, SubscriptionState> mSubscriptions = new HashMap<>();
 
     private static class SubscriptionState {
         private final Subscription mSubscription;
+
+        // A subscription must maintain a reference to any active nested subscription so that it may
+        // be later removed when the current subscription becomes invalid.
+        private Subscription.Token mNestedSubscriptionToken;
         private Boolean mAllConditionsMet;
+        private boolean mActive;
 
         SubscriptionState(Subscription subscription) {
             mSubscription = subscription;
@@ -54,7 +60,27 @@
             return mSubscription.mConditions;
         }
 
-        public void update() {
+        /**
+         * Signals that the {@link Subscription} is now being monitored and will receive updates
+         * based on its conditions.
+         */
+        private void setActive(boolean active) {
+            if (mActive == active) {
+                return;
+            }
+
+            mActive = active;
+
+            final Callback callback = mSubscription.getCallback();
+
+            if (callback == null) {
+                return;
+            }
+
+            callback.onActiveChanged(active);
+        }
+
+        public void update(Monitor monitor) {
             final Boolean result = Evaluator.INSTANCE.evaluate(mSubscription.mConditions,
                     Evaluator.OP_AND);
             // Consider unknown (null) as true
@@ -65,7 +91,50 @@
             }
 
             mAllConditionsMet = newAllConditionsMet;
-            mSubscription.mCallback.onConditionsChanged(mAllConditionsMet);
+
+            final Subscription nestedSubscription = mSubscription.getNestedSubscription();
+
+            if (nestedSubscription != null) {
+                if (mAllConditionsMet && mNestedSubscriptionToken == null) {
+                    // When all conditions are met for a subscription with a nested subscription
+                    // that is not currently being monitored, add the nested subscription for
+                    // monitor.
+                    mNestedSubscriptionToken =
+                            monitor.addSubscription(nestedSubscription, null);
+                } else if (!mAllConditionsMet && mNestedSubscriptionToken != null) {
+                    // When conditions are not met and there is an active nested condition, remove
+                    // the nested condition from monitoring.
+                    removeNestedSubscription(monitor);
+                }
+                return;
+            }
+
+            mSubscription.getCallback().onConditionsChanged(mAllConditionsMet);
+        }
+
+        /**
+         * Invoked when the {@link Subscription} has been added to the {@link Monitor}.
+         */
+        public void onAdded() {
+            setActive(true);
+        }
+
+        /**
+         * Invoked when the {@link Subscription} has been removed from the {@link Monitor},
+         * allowing cleanup code to run.
+         */
+        public void onRemoved(Monitor monitor) {
+            setActive(false);
+            removeNestedSubscription(monitor);
+        }
+
+        private void removeNestedSubscription(Monitor monitor) {
+            if (mNestedSubscriptionToken == null) {
+                return;
+            }
+
+            monitor.removeSubscription(mNestedSubscriptionToken);
+            mNestedSubscriptionToken = null;
         }
     }
 
@@ -77,9 +146,20 @@
         }
     };
 
+    /**
+     * Constructor for injected use-cases. By default, no preconditions are present.
+     */
     @Inject
     public Monitor(@Main Executor executor) {
+        this(executor, Collections.emptySet());
+    }
+
+    /**
+     * Main constructor, allowing specifying preconditions.
+     */
+    public Monitor(Executor executor, Set<Condition> preconditions) {
         mExecutor = executor;
+        mPreconditions = preconditions;
     }
 
     private void updateConditionMetState(Condition condition) {
@@ -91,7 +171,7 @@
             return;
         }
 
-        subscriptions.stream().forEach(token -> mSubscriptions.get(token).update());
+        subscriptions.stream().forEach(token -> mSubscriptions.get(token).update(this));
     }
 
     /**
@@ -101,15 +181,25 @@
      * @return A {@link Subscription.Token} that can be used to remove the subscription.
      */
     public Subscription.Token addSubscription(@NonNull Subscription subscription) {
+        return addSubscription(subscription, mPreconditions);
+    }
+
+    private Subscription.Token addSubscription(@NonNull Subscription subscription,
+            Set<Condition> preconditions) {
+        // If preconditions are set on the monitor, set up as a nested condition.
+        final Subscription normalizedCondition = preconditions != null
+                ? new Subscription.Builder(subscription).addConditions(preconditions).build()
+                : subscription;
+
         final Subscription.Token token = new Subscription.Token();
-        final SubscriptionState state = new SubscriptionState(subscription);
+        final SubscriptionState state = new SubscriptionState(normalizedCondition);
 
         mExecutor.execute(() -> {
             if (shouldLog()) Log.d(mTag, "adding subscription");
             mSubscriptions.put(token, state);
 
             // Add and associate conditions.
-            subscription.getConditions().stream().forEach(condition -> {
+            normalizedCondition.getConditions().stream().forEach(condition -> {
                 if (!mConditions.containsKey(condition)) {
                     mConditions.put(condition, new ArraySet<>());
                     condition.addCallback(mConditionCallback);
@@ -118,8 +208,10 @@
                 mConditions.get(condition).add(token);
             });
 
+            state.onAdded();
+
             // Update subscription state.
-            state.update();
+            state.update(this);
 
         });
         return token;
@@ -139,7 +231,9 @@
                 return;
             }
 
-            mSubscriptions.remove(token).getConditions().forEach(condition -> {
+            final SubscriptionState removedSubscription = mSubscriptions.remove(token);
+
+            removedSubscription.getConditions().forEach(condition -> {
                 if (!mConditions.containsKey(condition)) {
                     Log.e(mTag, "condition not present:" + condition);
                     return;
@@ -153,6 +247,8 @@
                     mConditions.remove(condition);
                 }
             });
+
+            removedSubscription.onRemoved(this);
         });
     }
 
@@ -168,12 +264,19 @@
         private final Set<Condition> mConditions;
         private final Callback mCallback;
 
-        /**
-         *
-         */
-        public Subscription(Set<Condition> conditions, Callback callback) {
+        // A nested {@link Subscription} is a special callback where the specified condition's
+        // active state is dependent on the conditions of the parent {@link Subscription} being met.
+        // Once active, the nested subscription's conditions are registered as normal with the
+        // monitor and its callback (which could also be a nested condition) is triggered based on
+        // those conditions. The nested condition will be removed from monitor if the outer
+        // subscription's conditions ever become invalid.
+        private final Subscription mNestedSubscription;
+
+        private Subscription(Set<Condition> conditions, Callback callback,
+                Subscription nestedSubscription) {
             this.mConditions = Collections.unmodifiableSet(conditions);
             this.mCallback = callback;
+            this.mNestedSubscription = nestedSubscription;
         }
 
         public Set<Condition> getConditions() {
@@ -184,6 +287,10 @@
             return mCallback;
         }
 
+        public Subscription getNestedSubscription() {
+            return mNestedSubscription;
+        }
+
         /**
          * A {@link Token} is an identifier that is associated with a {@link Subscription} which is
          * registered with a {@link Monitor}.
@@ -196,14 +303,26 @@
          */
         public static class Builder {
             private final Callback mCallback;
+            private final Subscription mNestedSubscription;
             private final ArraySet<Condition> mConditions;
+            private final ArraySet<Condition> mPreconditions;
 
             /**
              * Default constructor specifying the {@link Callback} for the {@link Subscription}.
              */
             public Builder(Callback callback) {
+                this(null, callback);
+            }
+
+            public Builder(Subscription nestedSubscription) {
+                this(nestedSubscription, null);
+            }
+
+            private Builder(Subscription nestedSubscription, Callback callback) {
+                mNestedSubscription = nestedSubscription;
                 mCallback = callback;
-                mConditions = new ArraySet<>();
+                mConditions = new ArraySet();
+                mPreconditions = new ArraySet();
             }
 
             /**
@@ -217,11 +336,38 @@
             }
 
             /**
+             * Adds a set of {@link Condition} to be a precondition for {@link Subscription}.
+             *
+             * @return The updated {@link Builder}.
+             */
+            public Builder addPreconditions(Set<Condition> condition) {
+                if (condition == null) {
+                    return this;
+                }
+                mPreconditions.addAll(condition);
+                return this;
+            }
+
+            /**
+             * Adds a {@link Condition} to be a precondition for {@link Subscription}.
+             *
+             * @return The updated {@link Builder}.
+             */
+            public Builder addPrecondition(Condition condition) {
+                mPreconditions.add(condition);
+                return this;
+            }
+
+            /**
              * Adds a set of {@link Condition} to be associated with the {@link Subscription}.
              *
              * @return The updated {@link Builder}.
              */
             public Builder addConditions(Set<Condition> condition) {
+                if (condition == null) {
+                    return this;
+                }
+
                 mConditions.addAll(condition);
                 return this;
             }
@@ -232,7 +378,11 @@
              * @return The resulting {@link Subscription}.
              */
             public Subscription build() {
-                return new Subscription(mConditions, mCallback);
+                final Subscription subscription =
+                        new Subscription(mConditions, mCallback, mNestedSubscription);
+                return !mPreconditions.isEmpty()
+                        ? new Subscription(mPreconditions, null, subscription)
+                        : subscription;
             }
         }
     }
@@ -255,5 +405,13 @@
          *                         only partial conditions have been fulfilled.
          */
         void onConditionsChanged(boolean allConditionsMet);
+
+        /**
+         * Called when the active state of the {@link Subscription} changes.
+         * @param active {@code true} when changes to the conditions will affect the
+         *               {@link Subscription}, {@code false} otherwise.
+         */
+        default void onActiveChanged(boolean active) {
+        }
     }
 }
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt
index ef2247f..9a581aa 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/regionsampling/RegionSampler.kt
@@ -114,7 +114,27 @@
 
     /** Dump region sampler */
     fun dump(pw: PrintWriter) {
-        regionSampler?.dump(pw)
+        pw.println("[RegionSampler]")
+        pw.println("regionSamplingEnabled: $regionSamplingEnabled")
+        pw.println("regionDarkness: $regionDarkness")
+        pw.println("lightForegroundColor: ${Integer.toHexString(lightForegroundColor)}")
+        pw.println("darkForegroundColor: ${Integer.toHexString(darkForegroundColor)}")
+        pw.println("passed-in sampledView: $sampledView")
+        pw.println("calculated samplingBounds: $samplingBounds")
+        pw.println(
+            "sampledView width: ${sampledView?.width}, sampledView height: ${sampledView?.height}"
+        )
+        pw.println("screen width: ${displaySize.x}, screen height: ${displaySize.y}")
+        pw.println(
+            "sampledRegionWithOffset: ${convertBounds(calculateSampledRegion(sampledView!!))}"
+        )
+        // TODO(b/265969235): mock initialSampling based on if component is on HS or LS wallpaper
+        // HS Smartspace - wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)
+        // LS Smartspace, clock - wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK)
+        pw.println(
+            "initialSampling for lockscreen: " +
+                "${wallpaperManager?.getWallpaperColors(WallpaperManager.FLAG_LOCK)}"
+        )
     }
 
     fun calculateSampledRegion(sampledView: View): RectF {
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt
index 9b73cc3..bd20777 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/shadow/DoubleShadowTextView.kt
@@ -25,7 +25,7 @@
 import com.android.systemui.shared.shadow.DoubleShadowTextHelper.applyShadows
 
 /** Extension of [TextView] which draws two shadows on the text (ambient and key shadows} */
-class DoubleShadowTextView
+open class DoubleShadowTextView
 @JvmOverloads
 constructor(
     context: Context,
diff --git a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
index ea079a9..1254e1e 100644
--- a/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
+++ b/packages/SystemUI/src/com/android/keyguard/ClockEventController.kt
@@ -24,8 +24,8 @@
 import android.text.format.DateFormat
 import android.util.TypedValue
 import android.view.View
-import android.widget.FrameLayout
 import android.view.ViewTreeObserver
+import android.widget.FrameLayout
 import androidx.annotation.VisibleForTesting
 import androidx.lifecycle.Lifecycle
 import androidx.lifecycle.repeatOnLifecycle
@@ -40,35 +40,37 @@
 import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
 import com.android.systemui.keyguard.shared.model.TransitionState
 import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.log.dagger.KeyguardSmallClockLog
 import com.android.systemui.log.dagger.KeyguardLargeClockLog
+import com.android.systemui.log.dagger.KeyguardSmallClockLog
 import com.android.systemui.plugins.ClockController
 import com.android.systemui.plugins.ClockFaceController
 import com.android.systemui.plugins.ClockTickRate
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel.DEBUG
 import com.android.systemui.shared.regionsampling.RegionSampler
-import com.android.systemui.statusbar.Weather
+import com.android.systemui.plugins.Weather
 import com.android.systemui.statusbar.policy.BatteryController
 import com.android.systemui.statusbar.policy.BatteryController.BatteryStateChangeCallback
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.util.concurrency.DelayableExecutor
+import java.util.Locale
+import java.util.TimeZone
+import java.util.concurrent.Executor
+import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.DisposableHandle
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.launch
-import java.util.Locale
-import java.util.TimeZone
-import java.util.concurrent.Executor
-import javax.inject.Inject
 
 /**
  * Controller for a Clock provided by the registry and used on the keyguard. Instantiated by
  * [KeyguardClockSwitchController]. Functionality is forked from [AnimatableClockController].
  */
-open class ClockEventController @Inject constructor(
+open class ClockEventController
+@Inject
+constructor(
     private val keyguardInteractor: KeyguardInteractor,
     private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
     private val broadcastDispatcher: BroadcastDispatcher,
@@ -115,52 +117,59 @@
     private var disposableHandle: DisposableHandle? = null
     private val regionSamplingEnabled = featureFlags.isEnabled(REGION_SAMPLING)
 
-    private val mLayoutChangedListener = object : View.OnLayoutChangeListener {
-        private var currentSmallClockView: View? = null
-        private var currentLargeClockView: View? = null
-        private var currentSmallClockLocation = IntArray(2)
-        private var currentLargeClockLocation = IntArray(2)
+    private val mLayoutChangedListener =
+        object : View.OnLayoutChangeListener {
+            private var currentSmallClockView: View? = null
+            private var currentLargeClockView: View? = null
+            private var currentSmallClockLocation = IntArray(2)
+            private var currentLargeClockLocation = IntArray(2)
 
-        override fun onLayoutChange(
-            view: View?,
-            left: Int,
-            top: Int,
-            right: Int,
-            bottom: Int,
-            oldLeft: Int,
-            oldTop: Int,
-            oldRight: Int,
-            oldBottom: Int
-        ) {
-        val parent = (view?.parent) as FrameLayout
+            override fun onLayoutChange(
+                view: View?,
+                left: Int,
+                top: Int,
+                right: Int,
+                bottom: Int,
+                oldLeft: Int,
+                oldTop: Int,
+                oldRight: Int,
+                oldBottom: Int
+            ) {
+                val parent = (view?.parent) as FrameLayout
 
-        // don't pass in negative bounds when clocks are in transition state
-        if (view.locationOnScreen[0] < 0 || view.locationOnScreen[1] < 0) {
-            return
-        }
+                // don't pass in negative bounds when clocks are in transition state
+                if (view.locationOnScreen[0] < 0 || view.locationOnScreen[1] < 0) {
+                    return
+                }
 
-        // SMALL CLOCK
-        if (parent.id == R.id.lockscreen_clock_view) {
-            // view bounds have changed due to clock size changing (i.e. different character widths)
-            // AND/OR the view has been translated when transitioning between small and large clock
-            if (view != currentSmallClockView ||
-                !view.locationOnScreen.contentEquals(currentSmallClockLocation)) {
-                currentSmallClockView = view
-                currentSmallClockLocation = view.locationOnScreen
-                updateRegionSampler(view)
+                // SMALL CLOCK
+                if (parent.id == R.id.lockscreen_clock_view) {
+                    // view bounds have changed due to clock size changing (i.e. different character
+                    // widths)
+                    // AND/OR the view has been translated when transitioning between small and
+                    // large clock
+                    if (
+                        view != currentSmallClockView ||
+                            !view.locationOnScreen.contentEquals(currentSmallClockLocation)
+                    ) {
+                        currentSmallClockView = view
+                        currentSmallClockLocation = view.locationOnScreen
+                        updateRegionSampler(view)
+                    }
+                }
+                // LARGE CLOCK
+                else if (parent.id == R.id.lockscreen_clock_view_large) {
+                    if (
+                        view != currentLargeClockView ||
+                            !view.locationOnScreen.contentEquals(currentLargeClockLocation)
+                    ) {
+                        currentLargeClockView = view
+                        currentLargeClockLocation = view.locationOnScreen
+                        updateRegionSampler(view)
+                    }
+                }
             }
         }
-        // LARGE CLOCK
-        else if (parent.id == R.id.lockscreen_clock_view_large) {
-            if (view != currentLargeClockView ||
-                !view.locationOnScreen.contentEquals(currentLargeClockLocation)) {
-                currentLargeClockView = view
-                currentLargeClockLocation = view.locationOnScreen
-                updateRegionSampler(view)
-            }
-        }
-        }
-    }
 
     private fun updateColors() {
         val wallpaperManager = WallpaperManager.getInstance(context)
@@ -189,30 +198,33 @@
 
     private fun updateRegionSampler(sampledRegion: View) {
         regionSampler?.stopRegionSampler()
-        regionSampler = createRegionSampler(
-            sampledRegion,
-            mainExecutor,
-            bgExecutor,
-            regionSamplingEnabled,
-            ::updateColors
-        )?.apply { startRegionSampler() }
+        regionSampler =
+            createRegionSampler(
+                    sampledRegion,
+                    mainExecutor,
+                    bgExecutor,
+                    regionSamplingEnabled,
+                    ::updateColors
+                )
+                ?.apply { startRegionSampler() }
 
         updateColors()
     }
 
     protected open fun createRegionSampler(
-            sampledView: View?,
-            mainExecutor: Executor?,
-            bgExecutor: Executor?,
-            regionSamplingEnabled: Boolean,
-            updateColors: () -> Unit
+        sampledView: View?,
+        mainExecutor: Executor?,
+        bgExecutor: Executor?,
+        regionSamplingEnabled: Boolean,
+        updateColors: () -> Unit
     ): RegionSampler? {
         return RegionSampler(
             sampledView,
             mainExecutor,
             bgExecutor,
             regionSamplingEnabled,
-            updateColors)
+            updateColors
+        )
     }
 
     var regionSampler: RegionSampler? = null
@@ -224,64 +236,68 @@
     private var smallClockIsDark = true
     private var largeClockIsDark = true
 
-    private val configListener = object : ConfigurationController.ConfigurationListener {
-        override fun onThemeChanged() {
-            clock?.events?.onColorPaletteChanged(resources)
-            updateColors()
-        }
-
-        override fun onDensityOrFontScaleChanged() {
-            updateFontSizes()
-        }
-    }
-
-    private val batteryCallback = object : BatteryStateChangeCallback {
-        override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
-            if (isKeyguardVisible && !isCharging && charging) {
-                clock?.animations?.charge()
+    private val configListener =
+        object : ConfigurationController.ConfigurationListener {
+            override fun onThemeChanged() {
+                clock?.events?.onColorPaletteChanged(resources)
+                updateColors()
             }
-            isCharging = charging
-        }
-    }
 
-    private val localeBroadcastReceiver = object : BroadcastReceiver() {
-        override fun onReceive(context: Context, intent: Intent) {
-            clock?.events?.onLocaleChanged(Locale.getDefault())
+            override fun onDensityOrFontScaleChanged() {
+                updateFontSizes()
+            }
         }
-    }
 
-    private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() {
-        override fun onKeyguardVisibilityChanged(visible: Boolean) {
-            isKeyguardVisible = visible
-            if (!featureFlags.isEnabled(DOZING_MIGRATION_1)) {
-                if (!isKeyguardVisible) {
-                    clock?.animations?.doze(if (isDozing) 1f else 0f)
+    private val batteryCallback =
+        object : BatteryStateChangeCallback {
+            override fun onBatteryLevelChanged(level: Int, pluggedIn: Boolean, charging: Boolean) {
+                if (isKeyguardVisible && !isCharging && charging) {
+                    clock?.animations?.charge()
+                }
+                isCharging = charging
+            }
+        }
+
+    private val localeBroadcastReceiver =
+        object : BroadcastReceiver() {
+            override fun onReceive(context: Context, intent: Intent) {
+                clock?.events?.onLocaleChanged(Locale.getDefault())
+            }
+        }
+
+    private val keyguardUpdateMonitorCallback =
+        object : KeyguardUpdateMonitorCallback() {
+            override fun onKeyguardVisibilityChanged(visible: Boolean) {
+                isKeyguardVisible = visible
+                if (!featureFlags.isEnabled(DOZING_MIGRATION_1)) {
+                    if (!isKeyguardVisible) {
+                        clock?.animations?.doze(if (isDozing) 1f else 0f)
+                    }
+                }
+
+                smallTimeListener?.update(shouldTimeListenerRun)
+                largeTimeListener?.update(shouldTimeListenerRun)
+            }
+
+            override fun onTimeFormatChanged(timeFormat: String) {
+                clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+            }
+
+            override fun onTimeZoneChanged(timeZone: TimeZone) {
+                clock?.events?.onTimeZoneChanged(timeZone)
+            }
+
+            override fun onUserSwitchComplete(userId: Int) {
+                clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
+            }
+
+            override fun onWeatherDataChanged(data: Weather?) {
+                if (data != null) {
+                    clock?.events?.onWeatherDataChanged(data)
                 }
             }
-
-            smallTimeListener?.update(shouldTimeListenerRun)
-            largeTimeListener?.update(shouldTimeListenerRun)
         }
 
-        override fun onTimeFormatChanged(timeFormat: String) {
-            clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
-        }
-
-        override fun onTimeZoneChanged(timeZone: TimeZone) {
-            clock?.events?.onTimeZoneChanged(timeZone)
-        }
-
-        override fun onUserSwitchComplete(userId: Int) {
-            clock?.events?.onTimeFormatChanged(DateFormat.is24HourFormat(context))
-        }
-
-        override fun onWeatherDataChanged(data: Weather?) {
-            if (data != null) {
-                clock?.events?.onWeatherDataChanged(data)
-            }
-        }
-    }
-
     fun registerListeners(parent: View) {
         if (isRegistered) {
             return
@@ -295,17 +311,18 @@
         configurationController.addCallback(configListener)
         batteryController.addCallback(batteryCallback)
         keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
-        disposableHandle = parent.repeatWhenAttached {
-            repeatOnLifecycle(Lifecycle.State.STARTED) {
-                listenForDozing(this)
-                if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
-                    listenForDozeAmountTransition(this)
-                    listenForAnyStateToAodTransition(this)
-                } else {
-                    listenForDozeAmount(this)
+        disposableHandle =
+            parent.repeatWhenAttached {
+                repeatOnLifecycle(Lifecycle.State.STARTED) {
+                    listenForDozing(this)
+                    if (featureFlags.isEnabled(DOZING_MIGRATION_1)) {
+                        listenForDozeAmountTransition(this)
+                        listenForAnyStateToAodTransition(this)
+                    } else {
+                        listenForDozeAmount(this)
+                    }
                 }
             }
-        }
         smallTimeListener?.update(shouldTimeListenerRun)
         largeTimeListener?.update(shouldTimeListenerRun)
     }
@@ -344,10 +361,18 @@
     }
 
     private fun updateFontSizes() {
-        clock?.smallClock?.events?.onFontSettingChanged(
-            resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat())
-        clock?.largeClock?.events?.onFontSettingChanged(
-            resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat())
+        clock
+            ?.smallClock
+            ?.events
+            ?.onFontSettingChanged(
+                resources.getDimensionPixelSize(R.dimen.small_clock_text_size).toFloat()
+            )
+        clock
+            ?.largeClock
+            ?.events
+            ?.onFontSettingChanged(
+                resources.getDimensionPixelSize(R.dimen.large_clock_text_size).toFloat()
+            )
     }
 
     private fun handleDoze(doze: Float) {
@@ -359,68 +384,59 @@
 
     @VisibleForTesting
     internal fun listenForDozeAmount(scope: CoroutineScope): Job {
-        return scope.launch {
-            keyguardInteractor.dozeAmount.collect {
-                handleDoze(it)
-            }
-        }
+        return scope.launch { keyguardInteractor.dozeAmount.collect { handleDoze(it) } }
     }
 
     @VisibleForTesting
     internal fun listenForDozeAmountTransition(scope: CoroutineScope): Job {
         return scope.launch {
-            keyguardTransitionInteractor.dozeAmountTransition.collect {
-                handleDoze(it.value)
-            }
+            keyguardTransitionInteractor.dozeAmountTransition.collect { handleDoze(it.value) }
         }
     }
 
     /**
-     * When keyguard is displayed again after being gone, the clock must be reset to full
-     * dozing.
+     * When keyguard is displayed again after being gone, the clock must be reset to full dozing.
      */
     @VisibleForTesting
     internal fun listenForAnyStateToAodTransition(scope: CoroutineScope): Job {
         return scope.launch {
-            keyguardTransitionInteractor.anyStateToAodTransition.filter {
-                it.transitionState == TransitionState.FINISHED
-            }.collect {
-                handleDoze(1f)
-            }
+            keyguardTransitionInteractor.anyStateToAodTransition
+                .filter { it.transitionState == TransitionState.FINISHED }
+                .collect { handleDoze(1f) }
         }
     }
 
     @VisibleForTesting
     internal fun listenForDozing(scope: CoroutineScope): Job {
         return scope.launch {
-            combine (
-                keyguardInteractor.dozeAmount,
-                keyguardInteractor.isDozing,
-            ) { localDozeAmount, localIsDozing ->
-                localDozeAmount > dozeAmount || localIsDozing
-            }
-            .collect { localIsDozing ->
-                isDozing = localIsDozing
-            }
+            combine(
+                    keyguardInteractor.dozeAmount,
+                    keyguardInteractor.isDozing,
+                ) { localDozeAmount, localIsDozing ->
+                    localDozeAmount > dozeAmount || localIsDozing
+                }
+                .collect { localIsDozing -> isDozing = localIsDozing }
         }
     }
 
     class TimeListener(val clockFace: ClockFaceController, val executor: DelayableExecutor) {
-        val predrawListener = ViewTreeObserver.OnPreDrawListener {
-            clockFace.events.onTimeTick()
-            true
-        }
-
-        val secondsRunnable = object : Runnable {
-            override fun run() {
-                if (!isRunning) {
-                    return
-                }
-
-                executor.executeDelayed(this, 990)
+        val predrawListener =
+            ViewTreeObserver.OnPreDrawListener {
                 clockFace.events.onTimeTick()
+                true
             }
-        }
+
+        val secondsRunnable =
+            object : Runnable {
+                override fun run() {
+                    if (!isRunning) {
+                        return
+                    }
+
+                    executor.executeDelayed(this, 990)
+                    clockFace.events.onTimeTick()
+                }
+            }
 
         var isRunning: Boolean = false
             private set
@@ -432,7 +448,9 @@
 
             isRunning = true
             when (clockFace.events.tickRate) {
-                ClockTickRate.PER_MINUTE -> {/* Handled by KeyguardClockSwitchController */}
+                ClockTickRate.PER_MINUTE -> {
+                    /* Handled by KeyguardClockSwitchController */
+                }
                 ClockTickRate.PER_SECOND -> executor.execute(secondsRunnable)
                 ClockTickRate.PER_FRAME -> {
                     clockFace.view.viewTreeObserver.addOnPreDrawListener(predrawListener)
@@ -442,7 +460,9 @@
         }
 
         fun stop() {
-            if (!isRunning) { return }
+            if (!isRunning) {
+                return
+            }
 
             isRunning = false
             clockFace.view.viewTreeObserver.removeOnPreDrawListener(predrawListener)
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt
index 1a06b5f..fe8b8c9 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardFaceListenModel.kt
@@ -27,6 +27,7 @@
     override var userId: Int = 0,
     override var listening: Boolean = false,
     // keep sorted
+    var alternateBouncerShowing: Boolean = false,
     var authInterruptActive: Boolean = false,
     var biometricSettingEnabledForUser: Boolean = false,
     var bouncerFullyShown: Boolean = false,
@@ -44,7 +45,6 @@
     var secureCameraLaunched: Boolean = false,
     var supportsDetect: Boolean = false,
     var switchingUser: Boolean = false,
-    var udfpsBouncerShowing: Boolean = false,
     var udfpsFingerDown: Boolean = false,
     var userNotTrustedOrDetectionIsNeeded: Boolean = false,
 ) : KeyguardListenModel() {
@@ -73,7 +73,7 @@
             secureCameraLaunched.toString(),
             supportsDetect.toString(),
             switchingUser.toString(),
-            udfpsBouncerShowing.toString(),
+            alternateBouncerShowing.toString(),
             udfpsFingerDown.toString(),
             userNotTrustedOrDetectionIsNeeded.toString(),
         )
@@ -95,6 +95,7 @@
                 userId = model.userId
                 listening = model.listening
                 // keep sorted
+                alternateBouncerShowing = model.alternateBouncerShowing
                 biometricSettingEnabledForUser = model.biometricSettingEnabledForUser
                 bouncerFullyShown = model.bouncerFullyShown
                 faceAndFpNotAuthenticated = model.faceAndFpNotAuthenticated
@@ -111,7 +112,6 @@
                 secureCameraLaunched = model.secureCameraLaunched
                 supportsDetect = model.supportsDetect
                 switchingUser = model.switchingUser
-                udfpsBouncerShowing = model.udfpsBouncerShowing
                 switchingUser = model.switchingUser
                 udfpsFingerDown = model.udfpsFingerDown
                 userNotTrustedOrDetectionIsNeeded = model.userNotTrustedOrDetectionIsNeeded
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index cd5b7c1..8820f02 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -147,12 +147,12 @@
 import com.android.systemui.dump.DumpManager;
 import com.android.systemui.dump.DumpsysTableLogger;
 import com.android.systemui.log.SessionTracker;
+import com.android.systemui.plugins.Weather;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.settings.UserTracker;
 import com.android.systemui.shared.system.TaskStackChangeListener;
 import com.android.systemui.shared.system.TaskStackChangeListeners;
 import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.Weather;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.policy.DevicePostureController;
 import com.android.systemui.telephony.TelephonyListenerManager;
@@ -260,6 +260,7 @@
     @VisibleForTesting
     public static final int BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED = -1;
     public static final int BIOMETRIC_HELP_FACE_NOT_RECOGNIZED = -2;
+    public static final int BIOMETRIC_HELP_FACE_NOT_AVAILABLE = -3;
 
     /**
      * If no cancel signal has been received after this amount of time, set the biometric running
@@ -310,7 +311,7 @@
     private boolean mGoingToSleep;
     private boolean mPrimaryBouncerFullyShown;
     private boolean mPrimaryBouncerIsOrWillBeShowing;
-    private boolean mUdfpsBouncerShowing;
+    private boolean mAlternateBouncerShowing;
     private boolean mAuthInterruptActive;
     private boolean mNeedsSlowUnlockTransition;
     private boolean mAssistantVisible;
@@ -535,7 +536,8 @@
      * It's assumed that the trust was granted for the current user.
      */
     private boolean shouldDismissKeyguardOnTrustGrantedWithCurrentUser(TrustGrantFlags flags) {
-        final boolean isBouncerShowing = mPrimaryBouncerIsOrWillBeShowing || mUdfpsBouncerShowing;
+        final boolean isBouncerShowing =
+                mPrimaryBouncerIsOrWillBeShowing || mAlternateBouncerShowing;
         return (flags.isInitiatedByUser() || flags.dismissKeyguardRequested())
                 && (mDeviceInteractive || flags.temporaryAndRenewable())
                 && (isBouncerShowing || flags.dismissKeyguardRequested());
@@ -1741,7 +1743,7 @@
                 public void onAuthenticationFailed() {
                         String reason =
                                 mKeyguardBypassController.canBypass() ? "bypass"
-                                        : mUdfpsBouncerShowing ? "udfpsBouncer"
+                                        : mAlternateBouncerShowing ? "alternateBouncer"
                                                 : mPrimaryBouncerFullyShown ? "bouncer"
                                                         : "udfpsFpDown";
                         requestActiveUnlock(
@@ -2600,7 +2602,7 @@
         requestActiveUnlock(
                 requestOrigin,
                 extraReason, canFaceBypass
-                        || mUdfpsBouncerShowing
+                        || mAlternateBouncerShowing
                         || mPrimaryBouncerFullyShown
                         || mAuthController.isUdfpsFingerDown());
     }
@@ -2618,23 +2620,23 @@
     }
 
     /**
-     * Whether the UDFPS bouncer is showing.
+     * Whether the alternate bouncer is showing.
      */
-    public void setUdfpsBouncerShowing(boolean showing) {
-        mUdfpsBouncerShowing = showing;
-        if (mUdfpsBouncerShowing) {
+    public void setAlternateBouncerShowing(boolean showing) {
+        mAlternateBouncerShowing = showing;
+        if (mAlternateBouncerShowing) {
             updateFaceListeningState(BIOMETRIC_ACTION_START,
                     FACE_AUTH_TRIGGERED_ALTERNATE_BIOMETRIC_BOUNCER_SHOWN);
             requestActiveUnlock(
                     ActiveUnlockConfig.ActiveUnlockRequestOrigin.UNLOCK_INTENT,
-                    "udfpsBouncer");
+                    "alternateBouncer");
         }
     }
 
     private boolean shouldTriggerActiveUnlock() {
         // Triggers:
         final boolean triggerActiveUnlockForAssistant = shouldTriggerActiveUnlockForAssistant();
-        final boolean awakeKeyguard = mPrimaryBouncerFullyShown || mUdfpsBouncerShowing
+        final boolean awakeKeyguard = mPrimaryBouncerFullyShown || mAlternateBouncerShowing
                 || (isKeyguardVisible() && !mGoingToSleep
                 && mStatusBarState != StatusBarState.SHADE_LOCKED);
 
@@ -2818,7 +2820,6 @@
         final boolean isPostureAllowedForFaceAuth =
                 mConfigFaceAuthSupportedPosture == 0 /* DEVICE_POSTURE_UNKNOWN */ ? true
                         : (mPostureState == mConfigFaceAuthSupportedPosture);
-
         // Only listen if this KeyguardUpdateMonitor belongs to the primary user. There is an
         // instance of KeyguardUpdateMonitor for each user but KeyguardUpdateMonitor is user-aware.
         final boolean shouldListen =
@@ -2828,11 +2829,11 @@
                         || awakeKeyguard
                         || shouldListenForFaceAssistant
                         || isUdfpsFingerDown
-                        || mUdfpsBouncerShowing)
+                        || mAlternateBouncerShowing)
                 && !mSwitchingUser && !faceDisabledForUser && userNotTrustedOrDetectionIsNeeded
                 && !mKeyguardGoingAway && biometricEnabledForUser
                 && faceAuthAllowedOrDetectionIsNeeded && mIsPrimaryUser
-                && (!mSecureCameraLaunched || mOccludingAppRequestingFace)
+                && (!mSecureCameraLaunched || mAlternateBouncerShowing)
                 && faceAndFpNotAuthenticated
                 && !mGoingToSleep
                 && isPostureAllowedForFaceAuth;
@@ -2843,6 +2844,7 @@
                     System.currentTimeMillis(),
                     user,
                     shouldListen,
+                    mAlternateBouncerShowing,
                     mAuthInterruptActive,
                     biometricEnabledForUser,
                     mPrimaryBouncerFullyShown,
@@ -2860,7 +2862,6 @@
                     mSecureCameraLaunched,
                     supportsDetect,
                     mSwitchingUser,
-                    mUdfpsBouncerShowing,
                     isUdfpsFingerDown,
                     userNotTrustedOrDetectionIsNeeded));
 
@@ -2948,9 +2949,26 @@
             // This would need to be updated for multi-sensor devices
             final boolean supportsFaceDetection = !mFaceSensorProperties.isEmpty()
                     && mFaceSensorProperties.get(0).supportsFaceDetection;
-            if (!isUnlockingWithBiometricAllowed(FACE) && supportsFaceDetection) {
-                mLogger.v("startListeningForFace - detect");
-                mFaceManager.detectFace(mFaceCancelSignal, mFaceDetectionCallback, userId);
+            if (!isUnlockingWithBiometricAllowed(FACE)) {
+                final boolean udfpsFingerprintAuthRunning = isUdfpsSupported()
+                        && isFingerprintDetectionRunning();
+                if (supportsFaceDetection && !udfpsFingerprintAuthRunning) {
+                    // Run face detection. (If a face is detected, show the bouncer.)
+                    mLogger.v("startListeningForFace - detect");
+                    mFaceManager.detectFace(mFaceCancelSignal, mFaceDetectionCallback, userId);
+                } else {
+                    // Don't run face detection. Instead, inform the user
+                    // face auth is unavailable and how to proceed.
+                    // (ie: "Use fingerprint instead" or "Swipe up to open")
+                    mLogger.v("Ignoring \"startListeningForFace - detect\". "
+                            + "Informing user face isn't available.");
+                    mFaceAuthenticationCallback.onAuthenticationHelp(
+                            BIOMETRIC_HELP_FACE_NOT_AVAILABLE,
+                            mContext.getResources().getString(
+                                    R.string.keyguard_face_unlock_unavailable)
+                    );
+                    return;
+                }
             } else {
                 mLogger.v("startListeningForFace - authenticate");
                 final boolean isBypassEnabled = mKeyguardBypassController != null
@@ -3954,7 +3972,7 @@
                 pw.println("        mPrimaryBouncerIsOrWillBeShowing="
                         + mPrimaryBouncerIsOrWillBeShowing);
                 pw.println("        mStatusBarState=" + StatusBarState.toString(mStatusBarState));
-                pw.println("        mUdfpsBouncerShowing=" + mUdfpsBouncerShowing);
+                pw.println("        mAlternateBouncerShowing=" + mAlternateBouncerShowing);
             } else if (isSfpsSupported()) {
                 pw.println("        sfpsEnrolled=" + isSfpsEnrolled());
                 pw.println("        shouldListenForSfps=" + shouldListenForFingerprint(false));
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index 4a7dd24..0da799e 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -23,8 +23,8 @@
 import androidx.annotation.Nullable;
 
 import com.android.settingslib.fuelgauge.BatteryStatus;
+import com.android.systemui.plugins.Weather;
 import com.android.systemui.statusbar.KeyguardIndicationController;
-import com.android.systemui.statusbar.Weather;
 
 import java.util.TimeZone;
 
diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
index 2c7eceb..379c78a 100644
--- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
+++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt
@@ -16,9 +16,11 @@
 
 package com.android.keyguard.logging
 
+import com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController
 import com.android.systemui.log.dagger.KeyguardLog
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel
+import com.android.systemui.statusbar.KeyguardIndicationController
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
@@ -76,4 +78,46 @@
             { "$str1 msgId: $str2 msg: $str3" }
         )
     }
+
+    fun logUpdateDeviceEntryIndication(
+        animate: Boolean,
+        visible: Boolean,
+        dozing: Boolean,
+    ) {
+        buffer.log(
+            KeyguardIndicationController.TAG,
+            LogLevel.DEBUG,
+            {
+                bool1 = animate
+                bool2 = visible
+                bool3 = dozing
+            },
+            { "updateDeviceEntryIndication animate:$bool1 visible:$bool2 dozing $bool3" }
+        )
+    }
+
+    fun logKeyguardSwitchIndication(
+        type: Int,
+        message: String?,
+    ) {
+        buffer.log(
+            KeyguardIndicationController.TAG,
+            LogLevel.DEBUG,
+            {
+                int1 = type
+                str1 = message
+            },
+            { "keyguardSwitchIndication ${getKeyguardSwitchIndicationNonSensitiveLog(int1, str1)}" }
+        )
+    }
+
+    fun getKeyguardSwitchIndicationNonSensitiveLog(type: Int, message: String?): String {
+        // only show the battery string. other strings may contain sensitive info
+        return if (type == KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BATTERY) {
+            "type=${KeyguardIndicationRotateTextViewController.indicationTypeToString(type)}" +
+                " message=$message"
+        } else {
+            "type=${KeyguardIndicationRotateTextViewController.indicationTypeToString(type)}"
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
index 27466d4..7509a8a 100644
--- a/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/controls/dagger/ControlsComponent.kt
@@ -19,11 +19,11 @@
 import android.content.Context
 import com.android.internal.widget.LockPatternUtils
 import com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT
-import com.android.systemui.controls.settings.ControlsSettingsRepository
 import com.android.systemui.controls.controller.ControlsController
 import com.android.systemui.controls.controller.ControlsTileResourceConfiguration
 import com.android.systemui.controls.controller.ControlsTileResourceConfigurationImpl
 import com.android.systemui.controls.management.ControlsListingController
+import com.android.systemui.controls.settings.ControlsSettingsRepository
 import com.android.systemui.controls.ui.ControlsUiController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.settings.UserTracker
diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
index ea9111f6..0078928 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt
@@ -512,7 +512,8 @@
     @JvmField val SCREENSHOT_METADATA = unreleasedFlag(1302, "screenshot_metadata", teamfood = true)
 
     // TODO(b/266955521): Tracking bug
-    @JvmField val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection")
+    @JvmField
+    val SCREENSHOT_DETECTION = unreleasedFlag(1303, "screenshot_detection", teamfood = true)
 
     // 1400 - columbus
     // TODO(b/254512756): Tracking Bug
@@ -524,7 +525,7 @@
 
     // 1500 - chooser aka sharesheet
     // TODO(b/254512507): Tracking Bug
-    val CHOOSER_UNBUNDLED = unreleasedFlag(1500, "chooser_unbundled", teamfood = true)
+    val CHOOSER_UNBUNDLED = releasedFlag(1500, "chooser_unbundled")
 
     // TODO(b/266983432) Tracking Bug
     val SHARESHEET_CUSTOM_ACTIONS =
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
index 9235e10..0745456 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewController.java
@@ -24,6 +24,7 @@
 
 import androidx.annotation.IntDef;
 
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.systemui.Dumpable;
 import com.android.systemui.dagger.qualifiers.Main;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
@@ -64,6 +65,7 @@
             2000L + KeyguardIndicationTextView.Y_IN_DURATION;
 
     private final StatusBarStateController mStatusBarStateController;
+    private final KeyguardLogger mLogger;
     private final float mMaxAlpha;
     private final ColorStateList mInitialTextColorState;
 
@@ -85,7 +87,8 @@
     public KeyguardIndicationRotateTextViewController(
             KeyguardIndicationTextView view,
             @Main DelayableExecutor executor,
-            StatusBarStateController statusBarStateController
+            StatusBarStateController statusBarStateController,
+            KeyguardLogger logger
     ) {
         super(view);
         mMaxAlpha = view.getAlpha();
@@ -93,6 +96,7 @@
         mInitialTextColorState = mView != null
                 ? mView.getTextColors() : ColorStateList.valueOf(Color.WHITE);
         mStatusBarStateController = statusBarStateController;
+        mLogger = logger;
         init();
     }
 
@@ -259,6 +263,8 @@
         mLastIndicationSwitch = SystemClock.uptimeMillis();
         if (!TextUtils.equals(previousMessage, mCurrMessage)
                 || previousIndicationType != mCurrIndicationType) {
+            mLogger.logKeyguardSwitchIndication(type,
+                    mCurrMessage != null ? mCurrMessage.toString() : null);
             mView.switchIndication(mIndicationMessages.get(type));
         }
 
@@ -352,9 +358,10 @@
     @Override
     public void dump(PrintWriter pw, String[] args) {
         pw.println("KeyguardIndicationRotatingTextViewController:");
-        pw.println("    currentMessage=" + mView.getText());
+        pw.println("    currentTextViewMessage=" + mView.getText());
+        pw.println("    currentStoredMessage=" + mView.getMessage());
         pw.println("    dozing:" + mIsDozing);
-        pw.println("    queue:" + mIndicationQueue.toString());
+        pw.println("    queue:" + mIndicationQueue);
         pw.println("    showNextIndicationRunnable:" + mShowNextIndicationRunnable);
 
         if (hasIndications()) {
@@ -398,4 +405,40 @@
     })
     @Retention(RetentionPolicy.SOURCE)
     public @interface IndicationType{}
+
+    /**
+     * Get human-readable string representation of the indication type.
+     */
+    public static String indicationTypeToString(@IndicationType int type) {
+        switch (type) {
+            case INDICATION_TYPE_NONE:
+                return "none";
+            case INDICATION_TYPE_DISCLOSURE:
+                return "disclosure";
+            case INDICATION_TYPE_OWNER_INFO:
+                return "owner_info";
+            case INDICATION_TYPE_LOGOUT:
+                return "logout";
+            case INDICATION_TYPE_BATTERY:
+                return "battery";
+            case INDICATION_TYPE_ALIGNMENT:
+                return "alignment";
+            case INDICATION_TYPE_TRANSIENT:
+                return "transient";
+            case INDICATION_TYPE_TRUST:
+                return "trust";
+            case INDICATION_TYPE_PERSISTENT_UNLOCK_MESSAGE:
+                return "persistent_unlock_message";
+            case INDICATION_TYPE_USER_LOCKED:
+                return "user_locked";
+            case INDICATION_TYPE_REVERSE_CHARGING:
+                return "reverse_charging";
+            case INDICATION_TYPE_BIOMETRIC_MESSAGE:
+                return "biometric_message";
+            case INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP:
+                return "biometric_message_followup";
+            default:
+                return "unknown[" + type + "]";
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 81a5828..8715d1f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -34,6 +34,7 @@
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
 
@@ -56,9 +57,14 @@
 
     private fun listenForDreamingToLockscreen() {
         scope.launch {
-            // Using isDreamingWithOverlay provides an optimized path to LOCKSCREEN state, which
-            // otherwise would have gone through OCCLUDED first
-            keyguardInteractor.isAbleToDream
+            // Dependending on the dream, either dream state or occluded change will change first,
+            // so listen for both
+            combine(keyguardInteractor.isAbleToDream, keyguardInteractor.isKeyguardOccluded) {
+                    isAbleToDream,
+                    isKeyguardOccluded ->
+                    isAbleToDream && isKeyguardOccluded
+                }
+                .distinctUntilChanged()
                 .sample(
                     combine(
                         keyguardInteractor.dozeTransitionModel,
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 3d39da6..7e86a5d 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
@@ -22,6 +22,8 @@
 import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging
 import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.flags.FeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.keyguard.shared.model.BiometricUnlockModel
 import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel
@@ -31,7 +33,6 @@
 import com.android.systemui.keyguard.shared.model.StatusBarState
 import com.android.systemui.keyguard.shared.model.WakefulnessModel
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.statusbar.CommandQueue.Callbacks
 import javax.inject.Inject
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.delay
@@ -41,7 +42,9 @@
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.onStart
 
 /**
  * Encapsulates business-logic related to the keyguard but not to a more specific part within it.
@@ -52,6 +55,7 @@
 constructor(
     private val repository: KeyguardRepository,
     private val commandQueue: CommandQueue,
+    featureFlags: FeatureFlags,
 ) {
     /**
      * The amount of doze the system is in, where `1.0` is fully dozing and `0.0` is not dozing at
@@ -129,6 +133,29 @@
      */
     val biometricUnlockState: Flow<BiometricUnlockModel> = repository.biometricUnlockState
 
+    /** Keyguard is present and is not occluded. */
+    val isKeyguardVisible: Flow<Boolean> =
+        combine(isKeyguardShowing, isKeyguardOccluded) { showing, occluded -> showing && !occluded }
+
+    /** Whether camera is launched over keyguard. */
+    var isSecureCameraActive =
+        if (featureFlags.isEnabled(Flags.FACE_AUTH_REFACTOR)) {
+            combine(
+                    isKeyguardVisible,
+                    repository.isBouncerShowing,
+                    onCameraLaunchDetected,
+                ) { isKeyguardVisible, isBouncerShowing, cameraLaunchEvent ->
+                    when {
+                        isKeyguardVisible -> false
+                        isBouncerShowing -> false
+                        else -> cameraLaunchEvent == CameraLaunchSourceModel.POWER_DOUBLE_TAP
+                    }
+                }
+                .onStart { emit(false) }
+        } else {
+            flowOf(false)
+        }
+
     /** The approximate location on the screen of the fingerprint sensor, if one is available. */
     val fingerprintSensorLocation: Flow<Point?> = repository.fingerprintSensorLocation
 
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 53c80f6..84bcdf9 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
@@ -42,6 +42,10 @@
 constructor(
     repository: KeyguardTransitionRepository,
 ) {
+    /** (any)->GONE transition information */
+    val anyStateToGoneTransition: Flow<TransitionStep> =
+        repository.transitions.filter { step -> step.to == KeyguardState.GONE }
+
     /** (any)->AOD transition information */
     val anyStateToAodTransition: Flow<TransitionStep> =
         repository.transitions.filter { step -> step.to == KeyguardState.AOD }
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
index 348d941..ccd4060 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/Diffable.kt
@@ -79,10 +79,10 @@
     }
 }
 
-/**
- * Each time the boolean flow is updated with a new value that's different from the previous value,
- * logs the new value to the given [tableLogBuffer].
- */
+// Here and below: Various Flow<SomeType> extension functions that are effectively equivalent to the
+// above [logDiffsForTable] method.
+
+/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
 fun Flow<Boolean>.logDiffsForTable(
     tableLogBuffer: TableLogBuffer,
     columnPrefix: String,
@@ -100,10 +100,8 @@
         newVal
     }
 }
-/**
- * Each time the Int flow is updated with a new value that's different from the previous value, logs
- * the new value to the given [tableLogBuffer].
- */
+
+/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
 fun Flow<Int>.logDiffsForTable(
     tableLogBuffer: TableLogBuffer,
     columnPrefix: String,
@@ -122,10 +120,26 @@
     }
 }
 
-/**
- * Each time the String? flow is updated with a new value that's different from the previous value,
- * logs the new value to the given [tableLogBuffer].
- */
+/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
+fun Flow<Int?>.logDiffsForTable(
+    tableLogBuffer: TableLogBuffer,
+    columnPrefix: String,
+    columnName: String,
+    initialValue: Int?,
+): Flow<Int?> {
+    val initialValueFun = {
+        tableLogBuffer.logChange(columnPrefix, columnName, initialValue)
+        initialValue
+    }
+    return this.pairwiseBy(initialValueFun) { prevVal, newVal: Int? ->
+        if (prevVal != newVal) {
+            tableLogBuffer.logChange(columnPrefix, columnName, newVal)
+        }
+        newVal
+    }
+}
+
+/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
 fun Flow<String?>.logDiffsForTable(
     tableLogBuffer: TableLogBuffer,
     columnPrefix: String,
@@ -143,3 +157,23 @@
         newVal
     }
 }
+
+/** See [logDiffsForTable(TableLogBuffer, String, T)]. */
+fun <T> Flow<List<T>>.logDiffsForTable(
+    tableLogBuffer: TableLogBuffer,
+    columnPrefix: String,
+    columnName: String,
+    initialValue: List<T>,
+): Flow<List<T>> {
+    val initialValueFun = {
+        tableLogBuffer.logChange(columnPrefix, columnName, initialValue.toString())
+        initialValue
+    }
+    return this.pairwiseBy(initialValueFun) { prevVal, newVal: List<T> ->
+        if (prevVal != newVal) {
+            // TODO(b/267761156): Can we log list changes without using toString?
+            tableLogBuffer.logChange(columnPrefix, columnName, newVal.toString())
+        }
+        newVal
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt
index 68c297f..4880f80 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableChange.kt
@@ -27,7 +27,7 @@
     var columnName: String = "",
     var type: DataType = DataType.EMPTY,
     var bool: Boolean = false,
-    var int: Int = 0,
+    var int: Int? = null,
     var str: String? = null,
 ) {
     /** Resets to default values so that the object can be recycled. */
@@ -54,7 +54,7 @@
     }
 
     /** Sets this to store an int change. */
-    fun set(value: Int) {
+    fun set(value: Int?) {
         type = DataType.INT
         int = value
     }
diff --git a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt
index 2c299d6..1712dab 100644
--- a/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt
+++ b/packages/SystemUI/src/com/android/systemui/log/table/TableLogBuffer.kt
@@ -138,7 +138,7 @@
     }
 
     /** Logs a Int change. */
-    fun logChange(prefix: String, columnName: String, value: Int) {
+    fun logChange(prefix: String, columnName: String, value: Int?) {
         logChange(systemClock.currentTimeMillis(), prefix, columnName, value)
     }
 
@@ -155,7 +155,7 @@
         change.set(value)
     }
 
-    private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int) {
+    private fun logChange(timestamp: Long, prefix: String, columnName: String, value: Int?) {
         val change = obtain(timestamp, prefix, columnName)
         change.set(value)
     }
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
index 0a94803..520edef 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaDataManager.kt
@@ -50,6 +50,7 @@
 import android.util.Log
 import androidx.media.utils.MediaConstants
 import com.android.internal.logging.InstanceId
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.Dumpable
 import com.android.systemui.R
 import com.android.systemui.broadcast.BroadcastDispatcher
@@ -67,6 +68,7 @@
 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
 import com.android.systemui.media.controls.resume.MediaResumeListener
+import com.android.systemui.media.controls.resume.ResumeMediaBrowser
 import com.android.systemui.media.controls.util.MediaControllerFactory
 import com.android.systemui.media.controls.util.MediaDataUtils
 import com.android.systemui.media.controls.util.MediaFlags
@@ -176,6 +178,7 @@
     private val mediaFlags: MediaFlags,
     private val logger: MediaUiEventLogger,
     private val smartspaceManager: SmartspaceManager,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
 ) : Dumpable, BcSmartspaceDataPlugin.SmartspaceTargetListener {
 
     companion object {
@@ -240,6 +243,7 @@
         mediaFlags: MediaFlags,
         logger: MediaUiEventLogger,
         smartspaceManager: SmartspaceManager,
+        keyguardUpdateMonitor: KeyguardUpdateMonitor,
     ) : this(
         context,
         backgroundExecutor,
@@ -263,6 +267,7 @@
         mediaFlags,
         logger,
         smartspaceManager,
+        keyguardUpdateMonitor,
     )
 
     private val appChangeReceiver =
@@ -1335,7 +1340,9 @@
         Assert.isMainThread()
         val removed = mediaEntries.remove(key) ?: return
 
-        if (useMediaResumption && removed.resumeAction != null && removed.isLocalSession()) {
+        if (keyguardUpdateMonitor.isUserInLockdown(removed.userId)) {
+            logger.logMediaRemoved(removed.appUid, removed.packageName, removed.instanceId)
+        } else if (useMediaResumption && removed.resumeAction != null && removed.isLocalSession()) {
             convertToResumePlayer(removed)
         } else if (mediaFlags.isRetainingPlayersEnabled()) {
             handlePossibleRemoval(removed, notificationRemoved = true)
@@ -1431,6 +1438,22 @@
             notifyMediaDataLoaded(key = pkg, oldKey = pkg, info = updated)
         }
         logger.logActiveConvertedToResume(updated.appUid, pkg, updated.instanceId)
+
+        // Limit total number of resume controls
+        val resumeEntries = mediaEntries.filter { (key, data) -> data.resumption }
+        val numResume = resumeEntries.size
+        if (numResume > ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
+            resumeEntries
+                .toList()
+                .sortedBy { (key, data) -> data.lastActive }
+                .subList(0, numResume - ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS)
+                .forEach { (key, data) ->
+                    Log.d(TAG, "Removing excess control $key")
+                    mediaEntries.remove(key)
+                    notifyMediaDataRemoved(key)
+                    logger.logMediaRemoved(data.appUid, data.packageName, data.instanceId)
+                }
+        }
     }
 
     fun setMediaResumptionEnabled(isEnabled: Boolean) {
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
index aa46b14..878962d 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/pipeline/MediaTimeoutListener.kt
@@ -40,7 +40,7 @@
 
 @VisibleForTesting
 val RESUME_MEDIA_TIMEOUT =
-    SystemProperties.getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(3))
+    SystemProperties.getLong("debug.sysui.media_timeout_resume", TimeUnit.DAYS.toMillis(2))
 
 /** Controller responsible for keeping track of playback states and expiring inactive streams. */
 @SysUISingleton
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
index fac1d5e..b72923a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaCarouselController.kt
@@ -30,13 +30,20 @@
 import android.view.animation.PathInterpolator
 import android.widget.LinearLayout
 import androidx.annotation.VisibleForTesting
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
 import com.android.internal.logging.InstanceId
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.Dumpable
 import com.android.systemui.R
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.lifecycle.repeatWhenAttached
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.models.player.MediaViewHolder
 import com.android.systemui.media.controls.models.recommendation.RecommendationViewHolder
@@ -63,6 +70,10 @@
 import java.util.TreeMap
 import javax.inject.Inject
 import javax.inject.Provider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.launch
 
 private const val TAG = "MediaCarouselController"
 private val settingsIntent = Intent().setAction(ACTION_MEDIA_CONTROLS_SETTINGS)
@@ -91,6 +102,8 @@
     private val logger: MediaUiEventLogger,
     private val debugLogger: MediaCarouselControllerLogger,
     private val mediaFlags: MediaFlags,
+    private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+    private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
 ) : Dumpable {
     /** The current width of the carousel */
     private var currentCarouselWidth: Int = 0
@@ -213,6 +226,17 @@
             }
         }
 
+    private val keyguardUpdateMonitorCallback =
+        object : KeyguardUpdateMonitorCallback() {
+            override fun onStrongAuthStateChanged(userId: Int) {
+                if (keyguardUpdateMonitor.isUserInLockdown(userId)) {
+                    hideMediaCarousel()
+                } else if (keyguardUpdateMonitor.isUserUnlocked(userId)) {
+                    showMediaCarousel()
+                }
+            }
+        }
+
     /**
      * Update MediaCarouselScrollHandler.visibleToUser to reflect media card container visibility.
      * It will be called when the container is out of view.
@@ -487,6 +511,13 @@
                 }
             }
         )
+        keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
+        mediaCarousel.repeatWhenAttached {
+            repeatOnLifecycle(Lifecycle.State.STARTED) {
+                // A backup to show media carousel (if available) once the keyguard is gone.
+                listenForAnyStateToGoneKeyguardTransition(this)
+            }
+        }
     }
 
     private fun inflateSettingsButton() {
@@ -516,6 +547,23 @@
         return mediaCarousel
     }
 
+    private fun hideMediaCarousel() {
+        mediaCarousel.visibility = View.GONE
+    }
+
+    private fun showMediaCarousel() {
+        mediaCarousel.visibility = View.VISIBLE
+    }
+
+    @VisibleForTesting
+    internal fun listenForAnyStateToGoneKeyguardTransition(scope: CoroutineScope): Job {
+        return scope.launch {
+            keyguardTransitionInteractor.anyStateToGoneTransition
+                .filter { it.transitionState == TransitionState.FINISHED }
+                .collect { showMediaCarousel() }
+        }
+    }
+
     private fun reorderAllPlayers(
         previousVisiblePlayerKey: MediaPlayerData.MediaSortKey?,
         key: String? = null
diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
index 0b4b668a..7f420a8 100644
--- a/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/media/controls/ui/MediaControlPanel.java
@@ -1136,8 +1136,10 @@
                         /* pixelDensity= */ getContext().getResources().getDisplayMetrics().density,
                         mColorSchemeTransition.getAccentPrimary().getCurrentColor(),
                         /* opacity= */ 100,
-                        /* shouldFillRipple= */ false,
                         /* sparkleStrength= */ 0f,
+                        /* baseRingFadeParams= */ null,
+                        /* sparkleRingFadeParams= */ null,
+                        /* centerFillFadeParams= */ null,
                         /* shouldDistort= */ false
                 )
         );
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
index 8000cd8..fab8c06 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/MediaTttChipControllerReceiver.kt
@@ -16,7 +16,9 @@
 
 package com.android.systemui.media.taptotransfer.receiver
 
+import android.animation.TimeInterpolator
 import android.annotation.SuppressLint
+import android.animation.ValueAnimator
 import android.app.StatusBarManager
 import android.content.Context
 import android.graphics.Rect
@@ -31,8 +33,10 @@
 import android.view.WindowManager
 import android.view.accessibility.AccessibilityManager
 import android.view.View.ACCESSIBILITY_LIVE_REGION_ASSERTIVE
+import android.view.View.ACCESSIBILITY_LIVE_REGION_NONE
 import com.android.internal.widget.CachingIconView
 import com.android.systemui.R
+import com.android.systemui.animation.Interpolators
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.ui.binder.TintedIconViewBinder
 import com.android.systemui.dagger.SysUISingleton
@@ -101,6 +105,13 @@
         fitInsetsTypes = 0 // Ignore insets from all system bars
     }
 
+    // Value animator that controls the bouncing animation of views.
+    private val bounceAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
+        repeatCount = ValueAnimator.INFINITE
+        repeatMode = ValueAnimator.REVERSE
+        duration = ICON_BOUNCE_ANIM_DURATION
+    }
+
     private val commandQueueCallbacks = object : CommandQueue.Callbacks {
         override fun updateMediaTapToTransferReceiverDisplay(
             @StatusBarManager.MediaTransferReceiverState displayState: Int,
@@ -203,44 +214,52 @@
 
         val iconView = currentView.getAppIconView()
         iconView.setPadding(iconPadding, iconPadding, iconPadding, iconPadding)
-        iconView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE
         TintedIconViewBinder.bind(iconInfo.toTintedIcon(), iconView)
+
+        val iconContainerView = currentView.getIconContainerView()
+        iconContainerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_ASSERTIVE
     }
 
     override fun animateViewIn(view: ViewGroup) {
-        val appIconView = view.getAppIconView()
+        val iconContainerView = view.getIconContainerView()
         val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple)
         val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
-        animateViewTranslationAndFade(appIconView, -1 * getTranslationAmount(), 1f)
-        animateViewTranslationAndFade(iconRippleView, -1 * getTranslationAmount(), 1f)
+        val translationYBy = getTranslationAmount()
+        // Make the icon container view starts animation from bottom of the screen.
+        iconContainerView.translationY += rippleController.getReceiverIconSize()
+        animateViewTranslationAndFade(
+            iconContainerView,
+            translationYBy = -1 * translationYBy,
+            alphaEndValue = 1f,
+            Interpolators.EMPHASIZED_DECELERATE,
+        ) {
+            animateBouncingView(iconContainerView, translationYBy * BOUNCE_TRANSLATION_RATIO)
+        }
         rippleController.expandToInProgressState(rippleView, iconRippleView)
     }
 
     override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
-        val appIconView = view.getAppIconView()
-        val iconRippleView: ReceiverChipRippleView = view.requireViewById(R.id.icon_glow_ripple)
+        val iconContainerView = view.getIconContainerView()
         val rippleView: ReceiverChipRippleView = view.requireViewById(R.id.ripple)
+        val translationYBy = getTranslationAmount()
+
+        // Remove update listeners from bounce animator to prevent any conflict with
+        // translation animation.
+        bounceAnimator.removeAllUpdateListeners()
+        bounceAnimator.cancel()
         if (removalReason == ChipStateReceiver.TRANSFER_TO_RECEIVER_SUCCEEDED.name &&
                 mediaTttFlags.isMediaTttReceiverSuccessRippleEnabled()) {
             rippleController.expandToSuccessState(rippleView, onAnimationEnd)
             animateViewTranslationAndFade(
-                iconRippleView,
-                -1 * getTranslationAmount(),
-                0f,
-                translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
-                alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
-            )
-            animateViewTranslationAndFade(
-                appIconView,
-                -1 * getTranslationAmount(),
+                iconContainerView,
+                -1 * translationYBy,
                 0f,
                 translationDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
                 alphaDuration = ICON_TRANSLATION_SUCCEEDED_DURATION,
             )
         } else {
             rippleController.collapseRipple(rippleView, onAnimationEnd)
-            animateViewTranslationAndFade(iconRippleView, getTranslationAmount(), 0f)
-            animateViewTranslationAndFade(appIconView, getTranslationAmount(), 0f)
+            animateViewTranslationAndFade(iconContainerView, translationYBy, 0f)
         }
     }
 
@@ -252,15 +271,19 @@
 
     /** Animation of view translation and fading. */
     private fun animateViewTranslationAndFade(
-        view: View,
+        view: ViewGroup,
         translationYBy: Float,
         alphaEndValue: Float,
+        interpolator: TimeInterpolator? = null,
         translationDuration: Long = ICON_TRANSLATION_ANIM_DURATION,
         alphaDuration: Long = ICON_ALPHA_ANIM_DURATION,
+        onAnimationEnd: Runnable? = null,
     ) {
         view.animate()
             .translationYBy(translationYBy)
+            .setInterpolator(interpolator)
             .setDuration(translationDuration)
+            .withEndAction { onAnimationEnd?.run() }
             .start()
         view.animate()
             .alpha(alphaEndValue)
@@ -270,17 +293,42 @@
 
     /** Returns the amount that the chip will be translated by in its intro animation. */
     private fun getTranslationAmount(): Float {
-        return rippleController.getRippleSize() * 0.5f -
-            rippleController.getReceiverIconSize()
+        return rippleController.getRippleSize() * 0.5f
     }
 
     private fun View.getAppIconView(): CachingIconView {
         return this.requireViewById(R.id.app_icon)
     }
 
+    private fun View.getIconContainerView(): ViewGroup {
+        return this.requireViewById(R.id.icon_container_view)
+    }
+
+    private fun animateBouncingView(iconContainerView: ViewGroup, translationYBy: Float) {
+        if (bounceAnimator.isStarted) {
+            return
+        }
+
+        addViewToBounceAnimation(iconContainerView, translationYBy)
+
+        // In order not to announce description every time the view animate.
+        iconContainerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE
+        bounceAnimator.start()
+    }
+
+    private fun addViewToBounceAnimation(view: View, translationYBy: Float) {
+        val prevTranslationY = view.translationY
+        bounceAnimator.addUpdateListener { updateListener ->
+            val progress = updateListener.animatedValue as Float
+            view.translationY = prevTranslationY + translationYBy * progress
+        }
+    }
+
     companion object {
         private const val ICON_TRANSLATION_ANIM_DURATION = 500L
+        private const val ICON_BOUNCE_ANIM_DURATION = 750L
         private const val ICON_TRANSLATION_SUCCEEDED_DURATION = 167L
+        private const val BOUNCE_TRANSLATION_RATIO = 0.15f
         private val ICON_ALPHA_ANIM_DURATION = 5.frames
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
index 997370b..4ff082a 100644
--- a/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
+++ b/packages/SystemUI/src/com/android/systemui/media/taptotransfer/receiver/ReceiverChipRippleView.kt
@@ -119,13 +119,19 @@
 
     private fun removeRippleFill() {
         with(rippleShader) {
+            // Set back to default because we modified them in [setupRippleFadeParams].
             baseRingFadeParams.fadeOutStart = RippleShader.DEFAULT_BASE_RING_FADE_OUT_START
             baseRingFadeParams.fadeOutEnd = RippleShader.DEFAULT_FADE_OUT_END
 
             centerFillFadeParams.fadeInStart = RippleShader.DEFAULT_FADE_IN_START
             centerFillFadeParams.fadeInEnd = RippleShader.DEFAULT_CENTER_FILL_FADE_IN_END
-            centerFillFadeParams.fadeOutStart = RippleShader.DEFAULT_CENTER_FILL_FADE_OUT_START
-            centerFillFadeParams.fadeOutEnd = RippleShader.DEFAULT_CENTER_FILL_FADE_OUT_END
+
+            // To avoid a seam showing up, we should match either:
+            // 1. baseRingFadeParams#fadeInEnd and centerFillFadeParams#fadeOutStart
+            // 2. baseRingFadeParams#fadeOutStart and centerFillFadeOutStart
+            // Here we go with 1 to fade in the centerFill faster.
+            centerFillFadeParams.fadeOutStart = baseRingFadeParams.fadeInEnd
+            centerFillFadeParams.fadeOutEnd = RippleShader.DEFAULT_FADE_OUT_END
         }
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
index 1678c6e..3088d8b 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorComponent.kt
@@ -46,10 +46,10 @@
 import dagger.Subcomponent
 import dagger.multibindings.ClassKey
 import dagger.multibindings.IntoMap
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.SupervisorJob
 import javax.inject.Qualifier
 import javax.inject.Scope
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
 
 @Qualifier @Retention(AnnotationRetention.BINARY) annotation class MediaProjectionAppSelector
 
@@ -110,6 +110,12 @@
         @Provides
         @MediaProjectionAppSelector
         @MediaProjectionAppSelectorScope
+        fun provideCallerPackageName(activity: MediaProjectionAppSelectorActivity): String? =
+            activity.callingPackage
+
+        @Provides
+        @MediaProjectionAppSelector
+        @MediaProjectionAppSelectorScope
         fun bindConfigurationController(
             activity: MediaProjectionAppSelectorActivity
         ): ConfigurationController = ConfigurationControllerImpl(activity)
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
index 52c7ca3..219629b 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorController.kt
@@ -36,16 +36,16 @@
     private val flags: FeatureFlags,
     @HostUserHandle private val hostUserHandle: UserHandle,
     @MediaProjectionAppSelector private val scope: CoroutineScope,
-    @MediaProjectionAppSelector private val appSelectorComponentName: ComponentName
+    @MediaProjectionAppSelector private val appSelectorComponentName: ComponentName,
+    @MediaProjectionAppSelector private val callerPackageName: String?
 ) {
 
     fun init() {
         scope.launch {
             val recentTasks = recentTaskListProvider.loadRecentTasks()
 
-            val tasks = recentTasks
-                .filterDevicePolicyRestrictedTasks()
-                .sortedTasks()
+            val tasks =
+                recentTasks.filterDevicePolicyRestrictedTasks().filterAppSelector().sortedTasks()
 
             view.bind(tasks)
         }
@@ -67,8 +67,13 @@
             filter { UserHandle.of(it.userId) == hostUserHandle }
         }
 
+    private fun List<RecentTask>.filterAppSelector(): List<RecentTask> = filter {
+        // Only take tasks that is not the app selector
+        it.topActivityComponent != appSelectorComponentName
+    }
+
     private fun List<RecentTask>.sortedTasks(): List<RecentTask> = sortedBy {
         // Show normal tasks first and only then tasks with opened app selector
-        it.topActivityComponent == appSelectorComponentName
+        it.topActivityComponent?.packageName == callerPackageName
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
new file mode 100644
index 0000000..7db293d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/process/ProcessWrapper.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 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.process;
+
+/**
+ * A simple wrapper that provides access to process-related details. This facilitates testing by
+ * providing a mockable target around these details.
+ */
+public class ProcessWrapper {
+    public int getUserHandleIdentifier() {
+        return android.os.Process.myUserHandle().getIdentifier();
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/process/condition/UserProcessCondition.java b/packages/SystemUI/src/com/android/systemui/process/condition/UserProcessCondition.java
new file mode 100644
index 0000000..5a21ea0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/process/condition/UserProcessCondition.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2023 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.process.condition;
+
+import com.android.systemui.process.ProcessWrapper;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shared.condition.Condition;
+
+import javax.inject.Inject;
+
+/**
+ * {@link UserProcessCondition} provides a signal when the process handle belongs to the current
+ * user.
+ */
+public class UserProcessCondition extends Condition {
+    private final ProcessWrapper mProcessWrapper;
+    private final UserTracker mUserTracker;
+
+    @Inject
+    public UserProcessCondition(ProcessWrapper processWrapper, UserTracker userTracker) {
+        mProcessWrapper = processWrapper;
+        mUserTracker = userTracker;
+    }
+
+    @Override
+    protected void start() {
+        updateCondition(mUserTracker.getUserId()
+                == mProcessWrapper.getUserHandleIdentifier());
+    }
+
+    @Override
+    protected void stop() {
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
index 628964a..25a5c61 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java
@@ -22,6 +22,7 @@
 import android.hardware.display.NightDisplayListener;
 import android.os.Handler;
 
+import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.media.dagger.MediaModule;
 import com.android.systemui.qs.AutoAddTracker;
@@ -53,6 +54,7 @@
 public interface QSModule {
 
     @Provides
+    @SysUISingleton
     static AutoTileManager provideAutoTileManager(
             Context context,
             AutoAddTracker.Builder autoAddTrackerBuilder,
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 41846f0..cd45b32 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -2144,6 +2144,7 @@
     void flingToHeight(float vel, boolean expand, float target,
             float collapseSpeedUpFactor, boolean expandBecauseOfFalsing) {
         mLastFlingWasExpanding = expand;
+        mShadeLog.logLastFlingWasExpanding(expand);
         mHeadsUpTouchHelper.notifyFling(!expand);
         mKeyguardStateController.notifyPanelFlingStart(!expand /* flingingToDismiss */);
         setClosingWithAlphaFadeout(!expand && !isOnKeyguard() && getFadeoutAlpha() == 1.0f);
@@ -4624,6 +4625,7 @@
         ipw.println(mBlockingExpansionForCurrentTouch);
         ipw.print("mExpectingSynthesizedDown="); ipw.println(mExpectingSynthesizedDown);
         ipw.print("mLastEventSynthesizedDown="); ipw.println(mLastEventSynthesizedDown);
+        ipw.print("mLastFlingWasExpanding="); ipw.println(mLastFlingWasExpanding);
         ipw.print("mInterpolatedDarkAmount="); ipw.println(mInterpolatedDarkAmount);
         ipw.print("mLinearDarkAmount="); ipw.println(mLinearDarkAmount);
         ipw.print("mPulsing="); ipw.println(mPulsing);
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
index 11617be..26c839de 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeLogger.kt
@@ -20,7 +20,6 @@
 import com.android.systemui.log.dagger.ShadeLog
 import com.android.systemui.plugins.log.LogBuffer
 import com.android.systemui.plugins.log.LogLevel
-import com.android.systemui.plugins.log.LogMessage
 import com.google.errorprone.annotations.CompileTimeConstant
 import javax.inject.Inject
 
@@ -234,4 +233,19 @@
             }
         )
     }
+
+    fun logLastFlingWasExpanding(
+            expand: Boolean
+    ) {
+        buffer.log(
+                TAG,
+                LogLevel.VERBOSE,
+                {
+                    bool1 = expand
+                },
+                {
+                    "NPVC mLastFlingWasExpanding set to: $bool1"
+                }
+        )
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index 4bf84f7..f4782c2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -25,6 +25,7 @@
 import static android.view.View.GONE;
 import static android.view.View.VISIBLE;
 
+import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_AVAILABLE;
 import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
 import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED;
 import static com.android.keyguard.KeyguardUpdateMonitor.getCurrentUser;
@@ -95,6 +96,7 @@
 import com.android.systemui.keyguard.ScreenLifecycle;
 import com.android.systemui.keyguard.domain.interactor.AlternateBouncerInteractor;
 import com.android.systemui.plugins.FalsingManager;
+import com.android.systemui.plugins.log.LogLevel;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
@@ -127,7 +129,7 @@
 @SysUISingleton
 public class KeyguardIndicationController {
 
-    private static final String TAG = "KeyguardIndication";
+    public static final String TAG = "KeyguardIndication";
     private static final boolean DEBUG_CHARGING_SPEED = false;
 
     private static final int MSG_SHOW_ACTION_TO_UNLOCK = 1;
@@ -327,9 +329,11 @@
         mInitialTextColorState = mTopIndicationView != null
                 ? mTopIndicationView.getTextColors() : ColorStateList.valueOf(Color.WHITE);
         mRotateTextViewController = new KeyguardIndicationRotateTextViewController(
-            mLockScreenIndicationView,
-            mExecutor,
-            mStatusBarStateController);
+                mLockScreenIndicationView,
+                mExecutor,
+                mStatusBarStateController,
+                mKeyguardLogger
+        );
         updateDeviceEntryIndication(false /* animate */);
         updateOrganizedOwnedDevice();
         if (mBroadcastReceiver == null) {
@@ -830,6 +834,7 @@
      * may continuously be cycled through.
      */
     protected final void updateDeviceEntryIndication(boolean animate) {
+        mKeyguardLogger.logUpdateDeviceEntryIndication(animate, mVisible, mDozing);
         if (!mVisible) {
             return;
         }
@@ -1080,18 +1085,23 @@
                 }
             }
 
+            final boolean faceAuthUnavailable = biometricSourceType == FACE
+                    && msgId == BIOMETRIC_HELP_FACE_NOT_AVAILABLE;
+
             // TODO(b/141025588): refactor to reduce repetition of code/comments
             // Only checking if unlocking with Biometric is allowed (no matter strong or non-strong
             // as long as primary auth, i.e. PIN/pattern/password, is not required), so it's ok to
             // pass true for isStrongBiometric to isUnlockingWithBiometricAllowed() to bypass the
             // check of whether non-strong biometric is allowed
             if (!mKeyguardUpdateMonitor
-                    .isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)) {
+                    .isUnlockingWithBiometricAllowed(true /* isStrongBiometric */)
+                    && !faceAuthUnavailable) {
                 return;
             }
 
             final boolean faceAuthSoftError = biometricSourceType == FACE
-                    && msgId != BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
+                    && msgId != BIOMETRIC_HELP_FACE_NOT_RECOGNIZED
+                    && msgId != BIOMETRIC_HELP_FACE_NOT_AVAILABLE;
             final boolean faceAuthFailed = biometricSourceType == FACE
                     && msgId == BIOMETRIC_HELP_FACE_NOT_RECOGNIZED; // ran through matcher & failed
             final boolean fpAuthFailed = biometricSourceType == FINGERPRINT
@@ -1134,6 +1144,13 @@
                             getTrustGrantedIndication(),
                             mContext.getString(R.string.keyguard_unlock)
                     );
+                } else if (faceAuthUnavailable) {
+                    showBiometricMessage(
+                            helpString,
+                            isUnlockWithFingerprintPossible
+                                    ? mContext.getString(R.string.keyguard_suggest_fingerprint)
+                                    : mContext.getString(R.string.keyguard_unlock)
+                    );
                 } else {
                     showBiometricMessage(helpString);
                 }
@@ -1417,6 +1434,7 @@
         public void onKeyguardShowingChanged() {
             // All transient messages are gone the next time keyguard is shown
             if (!mKeyguardStateController.isShowing()) {
+                mKeyguardLogger.log(TAG, LogLevel.DEBUG, "clear messages");
                 mTopIndicationView.clearMessages();
                 mRotateTextViewController.clearMessages();
             } else {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt
new file mode 100644
index 0000000..6148b40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerExt.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.plugins.statusbar.StatusBarStateController
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/** Returns a [Flow] that emits whenever [StatusBarStateController.isExpanded] changes value. */
+val StatusBarStateController.expansionChanges: Flow<Boolean>
+    get() = conflatedCallbackFlow {
+        val listener =
+            object : StatusBarStateController.StateListener {
+                override fun onExpandedChanged(isExpanded: Boolean) {
+                    trySend(isExpanded)
+                }
+            }
+        trySend(isExpanded)
+        addCallback(listener)
+        awaitClose { removeCallback(listener) }
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
index aed1324..6ef6165 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt
@@ -38,10 +38,12 @@
 import android.view.ViewGroup
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.settingslib.Utils
+import com.android.systemui.Dumpable
 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
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.ActivityStarter
@@ -56,12 +58,13 @@
 import com.android.systemui.shared.regionsampling.UpdateColorCallback
 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DATE_SMARTSPACE_DATA_PLUGIN
 import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.WEATHER_SMARTSPACE_DATA_PLUGIN
-import com.android.systemui.statusbar.Weather
+import com.android.systemui.plugins.Weather
 import com.android.systemui.statusbar.phone.KeyguardBypassController
 import com.android.systemui.statusbar.policy.ConfigurationController
 import com.android.systemui.statusbar.policy.DeviceProvisionedController
 import com.android.systemui.util.concurrency.Execution
 import com.android.systemui.util.settings.SecureSettings
+import java.io.PrintWriter
 import java.time.Instant
 import java.util.Optional
 import java.util.concurrent.Executor
@@ -86,6 +89,7 @@
         private val deviceProvisionedController: DeviceProvisionedController,
         private val bypassController: KeyguardBypassController,
         private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
+        private val dumpManager: DumpManager,
         private val execution: Execution,
         @Main private val uiExecutor: Executor,
         @Background private val bgExecutor: Executor,
@@ -96,7 +100,7 @@
         optionalWeatherPlugin: Optional<BcSmartspaceDataPlugin>,
         optionalPlugin: Optional<BcSmartspaceDataPlugin>,
         optionalConfigPlugin: Optional<BcSmartspaceConfigPlugin>,
-) {
+) : Dumpable {
     companion object {
         private const val TAG = "LockscreenSmartspaceController"
     }
@@ -230,6 +234,7 @@
 
     init {
         deviceProvisionedController.addCallback(deviceProvisionedListener)
+        dumpManager.registerDumpable(this)
     }
 
     fun isEnabled(): Boolean {
@@ -543,4 +548,11 @@
         }
         return null
     }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("Region Samplers: ${regionSamplers.size}")
+        regionSamplers.map { (_, sampler) ->
+            sampler.dump(pw)
+        }
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
index e996b78..6bf7668 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinator.kt
@@ -16,16 +16,15 @@
 
 package com.android.systemui.statusbar.notification.collection.coordinator
 
-import android.database.ContentObserver
 import android.os.UserHandle
 import android.provider.Settings
 import androidx.annotation.VisibleForTesting
-import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.keyguard.data.repository.KeyguardRepository
 import com.android.systemui.plugins.statusbar.StatusBarStateController
 import com.android.systemui.statusbar.StatusBarState
+import com.android.systemui.statusbar.expansionChanges
 import com.android.systemui.statusbar.notification.NotifPipelineFlags
 import com.android.systemui.statusbar.notification.collection.NotifPipeline
 import com.android.systemui.statusbar.notification.collection.NotificationEntry
@@ -35,15 +34,14 @@
 import com.android.systemui.statusbar.notification.collection.provider.SectionHeaderVisibilityProvider
 import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
+import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.statusbar.policy.headsUpEvents
 import com.android.systemui.util.settings.SecureSettings
-import com.android.systemui.util.settings.SettingsProxy
+import com.android.systemui.util.settings.SettingsProxyExt.observerFlow
 import javax.inject.Inject
-import kotlin.time.Duration.Companion.seconds
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.channels.awaitClose
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.coroutineScope
 import kotlinx.coroutines.flow.collectLatest
 import kotlinx.coroutines.flow.conflate
 import kotlinx.coroutines.flow.flowOn
@@ -60,6 +58,7 @@
 @Inject
 constructor(
     @Background private val bgDispatcher: CoroutineDispatcher,
+    private val headsUpManager: HeadsUpManager,
     private val keyguardNotificationVisibilityProvider: KeyguardNotificationVisibilityProvider,
     private val keyguardRepository: KeyguardRepository,
     private val notifPipelineFlags: NotifPipelineFlags,
@@ -87,28 +86,53 @@
     private fun attachUnseenFilter(pipeline: NotifPipeline) {
         pipeline.addFinalizeFilter(unseenNotifFilter)
         pipeline.addCollectionListener(collectionListener)
-        scope.launch { clearUnseenWhenKeyguardIsDismissed() }
+        scope.launch { trackUnseenNotificationsWhileUnlocked() }
         scope.launch { invalidateWhenUnseenSettingChanges() }
     }
 
-    private suspend fun clearUnseenWhenKeyguardIsDismissed() {
-        // Use collectLatest so that the suspending block is cancelled if isKeyguardShowing changes
-        // during the timeout period
+    private suspend fun trackUnseenNotificationsWhileUnlocked() {
+        // Use collectLatest so that trackUnseenNotifications() is cancelled when the keyguard is
+        // showing again
         keyguardRepository.isKeyguardShowing.collectLatest { isKeyguardShowing ->
             if (!isKeyguardShowing) {
                 unseenNotifFilter.invalidateList("keyguard no longer showing")
-                delay(SEEN_TIMEOUT)
+                trackUnseenNotifications()
+            }
+        }
+    }
+
+    private suspend fun trackUnseenNotifications() {
+        coroutineScope {
+            launch { clearUnseenNotificationsWhenShadeIsExpanded() }
+            launch { markHeadsUpNotificationsAsSeen() }
+        }
+    }
+
+    private suspend fun clearUnseenNotificationsWhenShadeIsExpanded() {
+        statusBarStateController.expansionChanges.collect { isExpanded ->
+            if (isExpanded) {
                 unseenNotifications.clear()
             }
         }
     }
 
+    private suspend fun markHeadsUpNotificationsAsSeen() {
+        headsUpManager.allEntries
+            .filter { it.isRowPinned }
+            .forEach { unseenNotifications.remove(it) }
+        headsUpManager.headsUpEvents.collect { (entry, isHun) ->
+            if (isHun) {
+                unseenNotifications.remove(entry)
+            }
+        }
+    }
+
     private suspend fun invalidateWhenUnseenSettingChanges() {
         secureSettings
             // emit whenever the setting has changed
-            .settingChangesForUser(
-                Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
+            .observerFlow(
                 UserHandle.USER_ALL,
+                Settings.Secure.LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS,
             )
             // perform a query immediately
             .onStart { emit(Unit) }
@@ -136,13 +160,17 @@
     private val collectionListener =
         object : NotifCollectionListener {
             override fun onEntryAdded(entry: NotificationEntry) {
-                if (keyguardRepository.isKeyguardShowing()) {
+                if (
+                    keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
+                ) {
                     unseenNotifications.add(entry)
                 }
             }
 
             override fun onEntryUpdated(entry: NotificationEntry) {
-                if (keyguardRepository.isKeyguardShowing()) {
+                if (
+                    keyguardRepository.isKeyguardShowing() || !statusBarStateController.isExpanded
+                ) {
                     unseenNotifications.add(entry)
                 }
             }
@@ -212,18 +240,5 @@
 
     companion object {
         private const val TAG = "KeyguardCoordinator"
-        private val SEEN_TIMEOUT = 5.seconds
     }
 }
-
-private fun SettingsProxy.settingChangesForUser(name: String, userHandle: Int): Flow<Unit> =
-    conflatedCallbackFlow {
-        val observer =
-            object : ContentObserver(null) {
-                override fun onChange(selfChange: Boolean) {
-                    trySend(Unit)
-                }
-            }
-        registerContentObserverForUser(name, observer, userHandle)
-        awaitClose { unregisterContentObserver(observer) }
-    }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index c534860..39e4000 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -28,8 +28,11 @@
 import android.content.ContextWrapper;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
+import android.content.res.Resources;
 import android.os.AsyncTask;
+import android.os.Build;
 import android.os.CancellationSignal;
+import android.os.Trace;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
 import android.util.Log;
@@ -38,6 +41,7 @@
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.widget.ImageMessageConsumer;
+import com.android.systemui.R;
 import com.android.systemui.dagger.SysUISingleton;
 import com.android.systemui.dagger.qualifiers.Background;
 import com.android.systemui.media.controls.util.MediaFeatureFlag;
@@ -468,6 +472,7 @@
                             result.packageContext,
                             parentLayout,
                             remoteViewClickHandler);
+                    validateView(v, entry, row.getResources());
                     v.setIsRootNamespace(true);
                     applyCallback.setResultView(v);
                 } else {
@@ -475,6 +480,7 @@
                             result.packageContext,
                             existingView,
                             remoteViewClickHandler);
+                    validateView(existingView, entry, row.getResources());
                     existingWrapper.onReinflated();
                 }
             } catch (Exception e) {
@@ -496,6 +502,13 @@
 
             @Override
             public void onViewApplied(View v) {
+                String invalidReason = isValidView(v, entry, row.getResources());
+                if (invalidReason != null) {
+                    handleInflationError(runningInflations, new InflationException(invalidReason),
+                            row.getEntry(), callback);
+                    runningInflations.remove(inflationId);
+                    return;
+                }
                 if (isNewView) {
                     v.setIsRootNamespace(true);
                     applyCallback.setResultView(v);
@@ -553,6 +566,65 @@
         runningInflations.put(inflationId, cancellationSignal);
     }
 
+    /**
+     * Checks if the given View is a valid notification View.
+     *
+     * @return null == valid, non-null == invalid, String represents reason for rejection.
+     */
+    @VisibleForTesting
+    @Nullable
+    static String isValidView(View view,
+            NotificationEntry entry,
+            Resources resources) {
+        if (!satisfiesMinHeightRequirement(view, entry, resources)) {
+            return "inflated notification does not meet minimum height requirement";
+        }
+        return null;
+    }
+
+    private static boolean satisfiesMinHeightRequirement(View view,
+            NotificationEntry entry,
+            Resources resources) {
+        if (!requiresHeightCheck(entry)) {
+            return true;
+        }
+        Trace.beginSection("NotificationContentInflater#satisfiesMinHeightRequirement");
+        int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+        int referenceWidth = resources.getDimensionPixelSize(
+                R.dimen.notification_validation_reference_width);
+        int widthSpec = View.MeasureSpec.makeMeasureSpec(referenceWidth, View.MeasureSpec.EXACTLY);
+        view.measure(widthSpec, heightSpec);
+        int minHeight = resources.getDimensionPixelSize(
+                R.dimen.notification_validation_minimum_allowed_height);
+        boolean result = view.getMeasuredHeight() >= minHeight;
+        Trace.endSection();
+        return result;
+    }
+
+    private static boolean requiresHeightCheck(NotificationEntry entry) {
+        // Undecorated custom views are disallowed from S onwards
+        if (entry.targetSdk >= Build.VERSION_CODES.S) {
+            return false;
+        }
+        // No need to check if the app isn't using any custom views
+        Notification notification = entry.getSbn().getNotification();
+        if (notification.contentView == null
+                && notification.bigContentView == null
+                && notification.headsUpContentView == null) {
+            return false;
+        }
+        return true;
+    }
+
+    private static void validateView(View view,
+            NotificationEntry entry,
+            Resources resources) throws InflationException {
+        String invalidReason = isValidView(view, entry, resources);
+        if (invalidReason != null) {
+            throw new InflationException(invalidReason);
+        }
+    }
+
     private static void handleInflationError(
             HashMap<Integer, CancellationSignal> runningInflations, Exception e,
             NotificationEntry notification, @Nullable InflationCallback callback) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
index 8b1a02b..576df7a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -29,6 +29,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.systemui.R;
 import com.android.systemui.dagger.qualifiers.Background;
+import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.AutoAddTracker;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.ReduceBrightColorsController;
@@ -47,6 +48,7 @@
 import com.android.systemui.util.settings.SecureSettings;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Objects;
 
 import javax.inject.Named;
@@ -165,9 +167,10 @@
         if (!mAutoTracker.isAdded(BRIGHTNESS) && mIsReduceBrightColorsAvailable) {
             mReduceBrightColorsController.addCallback(mReduceBrightColorsCallback);
         }
-        if (!mAutoTracker.isAdded(DEVICE_CONTROLS)) {
-            mDeviceControlsController.setCallback(mDeviceControlsCallback);
-        }
+        // We always want this callback, because if the feature stops being supported,
+        // we want to remove the tile from AutoAddTracker. That way it will be re-added when the
+        // feature is reenabled (similar to work tile).
+        mDeviceControlsController.setCallback(mDeviceControlsCallback);
         if (!mAutoTracker.isAdded(WALLET)) {
             initWalletController();
         }
@@ -323,14 +326,30 @@
         @Override
         public void onControlsUpdate(@Nullable Integer position) {
             if (mAutoTracker.isAdded(DEVICE_CONTROLS)) return;
-            if (position != null) {
+            if (position != null && !hasTile(DEVICE_CONTROLS)) {
                 mHost.addTile(DEVICE_CONTROLS, position);
+                mAutoTracker.setTileAdded(DEVICE_CONTROLS);
             }
-            mAutoTracker.setTileAdded(DEVICE_CONTROLS);
             mHandler.post(() -> mDeviceControlsController.removeCallback());
         }
+
+        @Override
+        public void removeControlsAutoTracker() {
+            mAutoTracker.setTileRemoved(DEVICE_CONTROLS);
+        }
     };
 
+    private boolean hasTile(String tileSpec) {
+        if (tileSpec == null) return false;
+        Collection<QSTile> tiles = mHost.getTiles();
+        for (QSTile tile : tiles) {
+            if (tileSpec.equals(tile.getTileSpec())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private void initWalletController() {
         if (mAutoTracker.isAdded(WALLET)) return;
         Integer position = mWalletController.getWalletPosition();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
index d24469e..b1553b0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardIndicationTextView.java
@@ -165,6 +165,13 @@
         }
     }
 
+    /**
+     * Get the message that should be shown after the previous text animates out.
+     */
+    public CharSequence getMessage() {
+        return mMessage;
+    }
+
     private AnimatorSet getOutAnimator() {
         AnimatorSet animatorSet = new AnimatorSet();
         Animator fadeOut = ObjectAnimator.ofFloat(this, View.ALPHA, 0f);
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 416bc71..5408afb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarIconControllerImpl.java
@@ -163,7 +163,8 @@
         for (int i = currentSlots.size() - 1; i >= 0; i--) {
             Slot s = currentSlots.get(i);
             slotsToReAdd.put(s, s.getHolderList());
-            removeAllIconsForSlot(s.getName());
+            // Don't force here because the new pipeline properly handles the tuner settings
+            removeAllIconsForSlot(s.getName(), /* force */ false);
         }
 
         // Add them all back
@@ -285,7 +286,7 @@
         // Because of the way we cache the icon holders, we need to remove everything any time
         // we get a new set of subscriptions. This might change in the future, but is required
         // to support demo mode for now
-        removeAllIconsForSlot(slotName);
+        removeAllIconsForSlot(slotName, /* force */ true);
 
         Collections.reverse(subIds);
 
@@ -428,6 +429,14 @@
     /** */
     @Override
     public void removeIcon(String slot, int tag) {
+        // If the new pipeline is on for this icon, don't allow removal, since the new pipeline
+        // will never call this method
+        if (mStatusBarPipelineFlags.isIconControlledByFlags(slot)) {
+            Log.i(TAG, "Ignoring removal of (" + slot + "). "
+                    + "It should be controlled elsewhere");
+            return;
+        }
+
         if (mStatusBarIconList.getIconHolder(slot, tag) == null) {
             return;
         }
@@ -444,6 +453,18 @@
     /** */
     @Override
     public void removeAllIconsForSlot(String slotName) {
+        removeAllIconsForSlot(slotName, /* force */ false);
+    }
+
+    private void removeAllIconsForSlot(String slotName, Boolean force) {
+        // If the new pipeline is on for this icon, don't allow removal, since the new pipeline
+        // will never call this method
+        if (!force && mStatusBarPipelineFlags.isIconControlledByFlags(slotName)) {
+            Log.i(TAG, "Ignoring removal of (" + slotName + "). "
+                    + "It should be controlled elsewhere");
+            return;
+        }
+
         Slot slot = mStatusBarIconList.getSlot(slotName);
         if (!slot.hasIconsInSlot()) {
             return;
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 fd46571..39281da 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -753,7 +753,7 @@
             mKeyguardMessageAreaController.setMessage("");
         }
         mBypassController.setAltBouncerShowing(isShowingAlternateBouncer);
-        mKeyguardUpdateManager.setUdfpsBouncerShowing(isShowingAlternateBouncer);
+        mKeyguardUpdateManager.setAlternateBouncerShowing(isShowingAlternateBouncer);
 
         if (updateScrim) {
             mCentralSurfaces.updateScrimController();
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 15fed32..4a684d9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/StatusBarPipelineFlags.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline
 
+import android.content.Context
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
@@ -23,7 +24,15 @@
 
 /** All flagging methods related to the new status bar pipeline (see b/238425913). */
 @SysUISingleton
-class StatusBarPipelineFlags @Inject constructor(private val featureFlags: FeatureFlags) {
+class StatusBarPipelineFlags
+@Inject
+constructor(
+    context: Context,
+    private val featureFlags: FeatureFlags,
+) {
+    private val mobileSlot = context.getString(com.android.internal.R.string.status_bar_mobile)
+    private val wifiSlot = context.getString(com.android.internal.R.string.status_bar_wifi)
+
     /** 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)
 
@@ -54,4 +63,13 @@
      */
     fun useDebugColoring(): Boolean =
         featureFlags.isEnabled(Flags.NEW_STATUS_BAR_ICONS_DEBUG_COLORING)
+
+    /**
+     * For convenience in the StatusBarIconController, we want to gate some actions based on slot
+     * name and the flag together.
+     *
+     * @return true if this icon is controlled by any of the status bar pipeline flags
+     */
+    fun isIconControlledByFlags(slotName: String): Boolean =
+        slotName == wifiSlot && useNewWifiIcon() || slotName == mobileSlot && useNewMobileIcons()
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileSummaryLog.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileSummaryLog.kt
new file mode 100644
index 0000000..2ac9ab3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/MobileSummaryLog.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 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.dagger
+
+import javax.inject.Qualifier
+
+/**
+ * Logs for mobile data that's **the same across all connections**.
+ *
+ * This buffer should only be used for the mobile parent classes like [MobileConnectionsRepository]
+ * and [MobileIconsInteractor]. It should *not* be used for classes that represent an individual
+ * connection, like [MobileConnectionRepository] or [MobileIconInteractor].
+ */
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class MobileSummaryLog
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
index 0993ab370..60de1a3 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/dagger/StatusBarPipelineModule.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepositoryImpl
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModel
 import com.android.systemui.statusbar.pipeline.airplane.ui.viewmodel.AirplaneModeViewModelImpl
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigCoreStartable
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileRepositorySwitcher
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
@@ -82,6 +83,11 @@
     @ClassKey(MobileUiAdapter::class)
     abstract fun bindFeature(impl: MobileUiAdapter): CoreStartable
 
+    @Binds
+    @IntoMap
+    @ClassKey(CarrierConfigCoreStartable::class)
+    abstract fun bindCarrierConfigStartable(impl: CarrierConfigCoreStartable): CoreStartable
+
     companion object {
         @Provides
         @SysUISingleton
@@ -112,5 +118,12 @@
         fun provideAirplaneTableLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
             return factory.create("AirplaneTableLog", 30)
         }
+
+        @Provides
+        @SysUISingleton
+        @MobileSummaryLog
+        fun provideMobileSummaryLogBuffer(factory: TableLogBufferFactory): TableLogBuffer {
+            return factory.create("MobileSummaryLog", 100)
+        }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt
index e618905..97a537a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/MobileConnectivityModel.kt
@@ -17,6 +17,8 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.model
 
 import android.net.NetworkCapabilities
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
 
 /** Provides information about a mobile network connection */
 data class MobileConnectivityModel(
@@ -24,4 +26,24 @@
     val isConnected: Boolean = false,
     /** Whether the mobile transport is validated [NetworkCapabilities.NET_CAPABILITY_VALIDATED] */
     val isValidated: Boolean = false,
-)
+) : Diffable<MobileConnectivityModel> {
+    // TODO(b/267767715): Can we implement [logDiffs] and [logFull] generically for data classes?
+    override fun logDiffs(prevVal: MobileConnectivityModel, row: TableRowLogger) {
+        if (prevVal.isConnected != isConnected) {
+            row.logChange(COL_IS_CONNECTED, isConnected)
+        }
+        if (prevVal.isValidated != isValidated) {
+            row.logChange(COL_IS_VALIDATED, isValidated)
+        }
+    }
+
+    override fun logFull(row: TableRowLogger) {
+        row.logChange(COL_IS_CONNECTED, isConnected)
+        row.logChange(COL_IS_VALIDATED, isValidated)
+    }
+
+    companion object {
+        private const val COL_IS_CONNECTED = "isConnected"
+        private const val COL_IS_VALIDATED = "isValidated"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
index c50d82a..78231e2 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/NetworkNameModel.kt
@@ -48,15 +48,31 @@
      * This name has been derived from telephony intents. see
      * [android.telephony.TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED]
      */
-    data class Derived(override val name: String) : NetworkNameModel {
+    data class IntentDerived(override val name: String) : NetworkNameModel {
         override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) {
-            if (prevVal !is Derived || prevVal.name != name) {
-                row.logChange(COL_NETWORK_NAME, "Derived($name)")
+            if (prevVal !is IntentDerived || prevVal.name != name) {
+                row.logChange(COL_NETWORK_NAME, "IntentDerived($name)")
             }
         }
 
         override fun logFull(row: TableRowLogger) {
-            row.logChange(COL_NETWORK_NAME, "Derived($name)")
+            row.logChange(COL_NETWORK_NAME, "IntentDerived($name)")
+        }
+    }
+
+    /**
+     * This name has been derived from the sim via
+     * [android.telephony.TelephonyManager.getSimOperatorName].
+     */
+    data class SimDerived(override val name: String) : NetworkNameModel {
+        override fun logDiffs(prevVal: NetworkNameModel, row: TableRowLogger) {
+            if (prevVal !is SimDerived || prevVal.name != name) {
+                row.logChange(COL_NETWORK_NAME, "SimDerived($name)")
+            }
+        }
+
+        override fun logFull(row: TableRowLogger) {
+            row.logChange(COL_NETWORK_NAME, "SimDerived($name)")
         }
     }
 
@@ -84,5 +100,5 @@
         str.append(spn)
     }
 
-    return if (str.isNotEmpty()) NetworkNameModel.Derived(str.toString()) else null
+    return if (str.isNotEmpty()) NetworkNameModel.IntentDerived(str.toString()) else null
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt
new file mode 100644
index 0000000..8c82fba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfig.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 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.mobile.data.model
+
+import android.os.PersistableBundle
+import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL
+import android.telephony.CarrierConfigManager.KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL
+import androidx.annotation.VisibleForTesting
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/**
+ * Represents, for a given subscription ID, the set of keys about which SystemUI cares.
+ *
+ * Upon first creation, this config represents only the default configuration (see
+ * [android.telephony.CarrierConfigManager.getDefaultConfig]).
+ *
+ * Upon request (see
+ * [com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository]), an
+ * instance of this class may be created for a given subscription Id, and will default to
+ * representing the default carrier configuration. However, once a carrier config is received for
+ * this [subId], all fields will reflect those in the received config, using [PersistableBundle]'s
+ * default of false for any config that is not present in the override.
+ *
+ * To keep things relatively simple, this class defines a wrapper around each config key which
+ * exposes a StateFlow<Boolean> for each config we care about. It also tracks whether or not it is
+ * using the default config for logging purposes.
+ *
+ * NOTE to add new keys to be tracked:
+ * 1. Define a new `private val` wrapping the key using [BooleanCarrierConfig]
+ * 2. Define a public `val` exposing the wrapped flow using [BooleanCarrierConfig.config]
+ * 3. Add the new [BooleanCarrierConfig] to the list of tracked configs, so they are properly
+ * updated when a new carrier config comes down
+ */
+class SystemUiCarrierConfig
+internal constructor(
+    val subId: Int,
+    defaultConfig: PersistableBundle,
+) {
+    @VisibleForTesting
+    var isUsingDefault = true
+        private set
+
+    private val inflateSignalStrength =
+        BooleanCarrierConfig(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, defaultConfig)
+    /** Flow tracking the [KEY_INFLATE_SIGNAL_STRENGTH_BOOL] carrier config */
+    val shouldInflateSignalStrength: StateFlow<Boolean> = inflateSignalStrength.config
+
+    private val showOperatorName =
+        BooleanCarrierConfig(KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL, defaultConfig)
+    /** Flow tracking the [KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL] config */
+    val showOperatorNameInStatusBar: StateFlow<Boolean> = showOperatorName.config
+
+    private val trackedConfigs =
+        listOf(
+            inflateSignalStrength,
+            showOperatorName,
+        )
+
+    /** Ingest a new carrier config, and switch all of the tracked keys over to the new values */
+    fun processNewCarrierConfig(config: PersistableBundle) {
+        isUsingDefault = false
+        trackedConfigs.forEach { it.update(config) }
+    }
+
+    /** For dumpsys, shortcut if we haven't overridden any keys */
+    fun toStringConsideringDefaults(): String {
+        return if (isUsingDefault) {
+            "using defaults"
+        } else {
+            trackedConfigs.joinToString { it.toString() }
+        }
+    }
+
+    override fun toString(): String = trackedConfigs.joinToString { it.toString() }
+}
+
+/** Extracts [key] from the carrier config, and stores it in a flow */
+private class BooleanCarrierConfig(
+    val key: String,
+    defaultConfig: PersistableBundle,
+) {
+    private val _configValue = MutableStateFlow(defaultConfig.getBoolean(key))
+    val config = _configValue.asStateFlow()
+
+    fun update(config: PersistableBundle) {
+        _configValue.value = config.getBoolean(key)
+    }
+
+    override fun toString(): String {
+        return "$key=${config.value}"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigCoreStartable.kt
new file mode 100644
index 0000000..af58999
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigCoreStartable.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.mobile.data.repository
+
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.qualifiers.Application
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Core startable which configures the [CarrierConfigRepository] to listen for updates for the
+ * lifetime of the process
+ */
+class CarrierConfigCoreStartable
+@Inject
+constructor(
+    private val carrierConfigRepository: CarrierConfigRepository,
+    @Application private val scope: CoroutineScope,
+) : CoreStartable {
+
+    override fun start() {
+        scope.launch { carrierConfigRepository.startObservingCarrierConfigUpdates() }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt
new file mode 100644
index 0000000..5769f90
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepository.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2023 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.mobile.data.repository
+
+import android.content.IntentFilter
+import android.os.PersistableBundle
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionManager
+import android.util.SparseArray
+import androidx.annotation.VisibleForTesting
+import androidx.core.util.getOrElse
+import androidx.core.util.isEmpty
+import androidx.core.util.keyIterator
+import com.android.systemui.Dumpable
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.shareIn
+
+/**
+ * Meant to be the source of truth regarding CarrierConfigs. These are configuration objects defined
+ * on a per-subscriptionId basis, and do not trigger a device configuration event.
+ *
+ * Designed to supplant [com.android.systemui.util.CarrierConfigTracker].
+ *
+ * See [SystemUiCarrierConfig] for details on how to add carrier config keys to be tracked
+ */
+@SysUISingleton
+class CarrierConfigRepository
+@Inject
+constructor(
+    broadcastDispatcher: BroadcastDispatcher,
+    private val carrierConfigManager: CarrierConfigManager,
+    dumpManager: DumpManager,
+    logger: ConnectivityPipelineLogger,
+    @Application scope: CoroutineScope,
+) : Dumpable {
+    private var isListening = false
+    private val defaultConfig: PersistableBundle by lazy { CarrierConfigManager.getDefaultConfig() }
+    // Used for logging the default config in the dumpsys
+    private val defaultConfigForLogs: SystemUiCarrierConfig by lazy {
+        SystemUiCarrierConfig(-1, defaultConfig)
+    }
+
+    private val configs = SparseArray<SystemUiCarrierConfig>()
+
+    init {
+        dumpManager.registerNormalDumpable(this)
+    }
+
+    @VisibleForTesting
+    val carrierConfigStream: SharedFlow<Pair<Int, PersistableBundle>> =
+        broadcastDispatcher
+            .broadcastFlow(IntentFilter(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
+                intent,
+                _ ->
+                intent.getIntExtra(
+                    CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX,
+                    SubscriptionManager.INVALID_SUBSCRIPTION_ID
+                )
+            }
+            .onEach { logger.logCarrierConfigChanged(it) }
+            .filter { SubscriptionManager.isValidSubscriptionId(it) }
+            .mapNotNull { subId ->
+                val config = carrierConfigManager.getConfigForSubId(subId)
+                config?.let { subId to it }
+            }
+            .shareIn(scope, SharingStarted.WhileSubscribed())
+
+    /**
+     * Start this repository observing broadcasts for **all** carrier configuration updates. Must be
+     * called in order to keep SystemUI in sync with [CarrierConfigManager].
+     */
+    suspend fun startObservingCarrierConfigUpdates() {
+        isListening = true
+        carrierConfigStream.collect { updateCarrierConfig(it.first, it.second) }
+    }
+
+    /** Update or create the [SystemUiCarrierConfig] for subId with the override */
+    private fun updateCarrierConfig(subId: Int, config: PersistableBundle) {
+        val configToUpdate = getOrCreateConfigForSubId(subId)
+        configToUpdate.processNewCarrierConfig(config)
+    }
+
+    /** Gets a cached [SystemUiCarrierConfig], or creates a new one which will track the defaults */
+    fun getOrCreateConfigForSubId(subId: Int): SystemUiCarrierConfig {
+        return configs.getOrElse(subId) {
+            val config = SystemUiCarrierConfig(subId, defaultConfig)
+            val carrierConfig = carrierConfigManager.getConfigForSubId(subId)
+            if (carrierConfig != null) config.processNewCarrierConfig(carrierConfig)
+            configs.put(subId, config)
+            config
+        }
+    }
+
+    override fun dump(pw: PrintWriter, args: Array<out String>) {
+        pw.println("isListening: $isListening")
+        if (configs.isEmpty()) {
+            pw.println("no carrier configs loaded")
+        } else {
+            pw.println("Carrier configs by subId")
+            configs.keyIterator().forEach {
+                pw.println("  subId=$it")
+                pw.println("    config=${configs.get(it).toStringConsideringDefaults()}")
+            }
+            // Finally, print the default config
+            pw.println("Default config:")
+            pw.println("  $defaultConfigForLogs")
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
index e0d156a..c640baa 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileConnectionsRepository.kt
@@ -16,7 +16,6 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository
 
-import android.provider.Settings
 import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionManager
 import com.android.settingslib.SignalIcon.MobileIconGroup
@@ -53,9 +52,6 @@
     /** Get or create a repository for the line of service for the given subscription ID */
     fun getRepoForSubId(subId: Int): MobileConnectionRepository
 
-    /** Observe changes to the [Settings.Global.MOBILE_DATA] setting */
-    val globalMobileDataSettingChangedEvent: Flow<Unit>
-
     /**
      * [Config] is an object that tracks relevant configuration flags for a given subscription ID.
      * In the case of [MobileMappings], it's hard-coded to check the default data subscription's
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
index b939856..7038a3b 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcher.kt
@@ -156,9 +156,6 @@
                 realRepository.defaultMobileNetworkConnectivity.value
             )
 
-    override val globalMobileDataSettingChangedEvent: Flow<Unit> =
-        activeRepo.flatMapLatest { it.globalMobileDataSettingChangedEvent }
-
     override fun getRepoForSubId(subId: Int): MobileConnectionRepository {
         if (isDemoMode.value) {
             return demoMobileConnectionsRepository.getRepoForSubId(subId)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
index 1088345..d10d11d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepository.kt
@@ -185,8 +185,6 @@
         return CacheContainer(repo, lastMobileState = null)
     }
 
-    override val globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
-
     fun startProcessingCommands() {
         mobileDemoCommandJob =
             scope.launch {
@@ -242,7 +240,7 @@
 
         // This is always true here, because we split out disabled states at the data-source level
         connection.dataEnabled.value = true
-        connection.networkName.value = NetworkNameModel.Derived(state.name)
+        connection.networkName.value = NetworkNameModel.IntentDerived(state.name)
 
         connection.cdmaRoaming.value = state.roaming
         connection.connectionInfo.value = state.toMobileConnectionModel()
@@ -260,10 +258,13 @@
         maybeCreateSubscription(subId)
         carrierMergedSubId = subId
 
+        // TODO(b/261029387): until we have a command, use the most recent subId
+        defaultDataSubId.value = subId
+
         val connection = getRepoForSubId(subId)
         // This is always true here, because we split out disabled states at the data-source level
         connection.dataEnabled.value = true
-        connection.networkName.value = NetworkNameModel.Derived(CARRIER_MERGED_NAME)
+        connection.networkName.value = NetworkNameModel.IntentDerived(CARRIER_MERGED_NAME)
         connection.numberOfLevels.value = event.numberOfLevels
         connection.cdmaRoaming.value = false
         connection.connectionInfo.value = event.toMobileConnectionModel()
@@ -338,7 +339,10 @@
     }
 
     private fun FakeWifiEventModel.CarrierMerged.toMobileConnectionModel(): MobileConnectionModel {
-        return createCarrierMergedConnectionModel(this.level)
+        return createCarrierMergedConnectionModel(
+            this.level,
+            activity.toMobileDataActivityModel(),
+        )
     }
 
     private fun SignalIcon.MobileIconGroup?.toResolvedNetworkType(): ResolvedNetworkType {
@@ -373,5 +377,5 @@
 
     override val cdmaRoaming = MutableStateFlow(false)
 
-    override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo network"))
+    override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo network"))
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
index c783b12..938c734 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepository.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
+import android.telephony.TelephonyManager
 import android.util.Log
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
@@ -37,7 +38,6 @@
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 
@@ -54,10 +54,18 @@
 class CarrierMergedConnectionRepository(
     override val subId: Int,
     override val tableLogBuffer: TableLogBuffer,
-    defaultNetworkName: NetworkNameModel,
+    private val telephonyManager: TelephonyManager,
     @Application private val scope: CoroutineScope,
     val wifiRepository: WifiRepository,
 ) : MobileConnectionRepository {
+    init {
+        if (telephonyManager.subscriptionId != subId) {
+            throw IllegalStateException(
+                "CarrierMergedRepo: TelephonyManager should be created with subId($subId). " +
+                    "Found ${telephonyManager.subscriptionId} instead."
+            )
+        }
+    }
 
     /**
      * Outputs the carrier merged network to use, or null if we don't have a valid carrier merged
@@ -87,20 +95,28 @@
         }
 
     override val connectionInfo: StateFlow<MobileConnectionModel> =
-        network
-            .map { it.toMobileConnectionModel() }
+        combine(network, wifiRepository.wifiActivity) { network, activity ->
+                if (network == null) {
+                    MobileConnectionModel()
+                } else {
+                    createCarrierMergedConnectionModel(network.level, activity)
+                }
+            }
             .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectionModel())
 
-    // TODO(b/238425913): Add logging to this class.
-    // TODO(b/238425913): Make sure SignalStrength.getEmptyState is used when appropriate.
+    override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(ROAMING).asStateFlow()
 
-    // Carrier merged is never roaming.
-    override val cdmaRoaming: StateFlow<Boolean> = MutableStateFlow(false).asStateFlow()
-
-    // TODO(b/238425913): Fetch the carrier merged network name.
     override val networkName: StateFlow<NetworkNameModel> =
-        flowOf(defaultNetworkName)
-            .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
+        network
+            // The SIM operator name should be the same throughout the lifetime of a subId, **but**
+            // it may not be available when this repo is created because it takes time to load. To
+            // be safe, we re-fetch it each time the network has changed.
+            .map { NetworkNameModel.SimDerived(telephonyManager.simOperatorName) }
+            .stateIn(
+                scope,
+                SharingStarted.WhileSubscribed(),
+                NetworkNameModel.SimDerived(telephonyManager.simOperatorName),
+            )
 
     override val numberOfLevels: StateFlow<Int> =
         wifiRepository.wifiNetwork
@@ -115,37 +131,24 @@
 
     override val dataEnabled: StateFlow<Boolean> = wifiRepository.isWifiEnabled
 
-    private fun WifiNetworkModel.CarrierMerged?.toMobileConnectionModel(): MobileConnectionModel {
-        if (this == null) {
-            return MobileConnectionModel()
-        }
-
-        return createCarrierMergedConnectionModel(level)
-    }
-
     companion object {
         /**
          * Creates an instance of [MobileConnectionModel] that represents a carrier merged network
-         * with the given [level].
+         * with the given [level] and [activity].
          */
-        fun createCarrierMergedConnectionModel(level: Int): MobileConnectionModel {
+        fun createCarrierMergedConnectionModel(
+            level: Int,
+            activity: DataActivityModel,
+        ): MobileConnectionModel {
             return MobileConnectionModel(
                 primaryLevel = level,
                 cdmaLevel = level,
-                // A [WifiNetworkModel.CarrierMerged] instance is always connected.
-                // (A [WifiNetworkModel.Inactive] represents a disconnected network.)
-                dataConnectionState = DataConnectionState.Connected,
-                // TODO(b/238425913): This should come from [WifiRepository.wifiActivity].
-                dataActivityDirection =
-                    DataActivityModel(
-                        hasActivityIn = false,
-                        hasActivityOut = false,
-                    ),
+                dataActivityDirection = activity,
+                // Here and below: These values are always the same for every carrier-merged
+                // connection.
                 resolvedNetworkType = ResolvedNetworkType.CarrierMergedNetworkType,
-                // Carrier merged is never roaming
-                isRoaming = false,
-
-                // TODO(b/238425913): Verify that these fields never change for carrier merged.
+                dataConnectionState = DataConnectionState.Connected,
+                isRoaming = ROAMING,
                 isEmergencyOnly = false,
                 operatorAlphaShort = null,
                 isInService = true,
@@ -153,24 +156,27 @@
                 carrierNetworkChangeActive = false,
             )
         }
+
+        // Carrier merged is never roaming
+        private const val ROAMING = false
     }
 
     @SysUISingleton
     class Factory
     @Inject
     constructor(
+        private val telephonyManager: TelephonyManager,
         @Application private val scope: CoroutineScope,
         private val wifiRepository: WifiRepository,
     ) {
         fun build(
             subId: Int,
             mobileLogger: TableLogBuffer,
-            defaultNetworkName: NetworkNameModel,
         ): MobileConnectionRepository {
             return CarrierMergedConnectionRepository(
                 subId,
                 mobileLogger,
-                defaultNetworkName,
+                telephonyManager.createForSubscriptionId(subId),
                 scope,
                 wifiRepository,
             )
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
index 0f30ae2..a39ea0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepository.kt
@@ -26,7 +26,6 @@
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
@@ -50,7 +49,6 @@
     override val tableLogBuffer: TableLogBuffer,
     private val defaultNetworkName: NetworkNameModel,
     private val networkNameSeparator: String,
-    private val globalMobileDataSettingChangedEvent: Flow<Unit>,
     @Application scope: CoroutineScope,
     private val mobileRepoFactory: MobileConnectionRepositoryImpl.Factory,
     private val carrierMergedRepoFactory: CarrierMergedConnectionRepository.Factory,
@@ -84,12 +82,11 @@
             tableLogBuffer,
             defaultNetworkName,
             networkNameSeparator,
-            globalMobileDataSettingChangedEvent,
         )
     }
 
     private val carrierMergedRepo: MobileConnectionRepository by lazy {
-        carrierMergedRepoFactory.build(subId, tableLogBuffer, defaultNetworkName)
+        carrierMergedRepoFactory.build(subId, tableLogBuffer)
     }
 
     @VisibleForTesting
@@ -120,11 +117,22 @@
     override val connectionInfo =
         activeRepo
             .flatMapLatest { it.connectionInfo }
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                initialValue = activeRepo.value.connectionInfo.value,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.connectionInfo.value)
 
     override val dataEnabled =
         activeRepo
             .flatMapLatest { it.dataEnabled }
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                columnName = "dataEnabled",
+                initialValue = activeRepo.value.dataEnabled.value,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.dataEnabled.value)
 
     override val numberOfLevels =
@@ -135,6 +143,11 @@
     override val networkName =
         activeRepo
             .flatMapLatest { it.networkName }
+            .logDiffsForTable(
+                tableLogBuffer,
+                columnPrefix = "",
+                initialValue = activeRepo.value.networkName.value,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), activeRepo.value.networkName.value)
 
     class Factory
@@ -150,7 +163,6 @@
             startingIsCarrierMerged: Boolean,
             defaultNetworkName: NetworkNameModel,
             networkNameSeparator: String,
-            globalMobileDataSettingChangedEvent: Flow<Unit>,
         ): FullMobileConnectionRepository {
             val mobileLogger =
                 logFactory.getOrCreate(tableBufferLogName(subId), MOBILE_CONNECTION_BUFFER_SIZE)
@@ -161,7 +173,6 @@
                 mobileLogger,
                 defaultNetworkName,
                 networkNameSeparator,
-                globalMobileDataSettingChangedEvent,
                 scope,
                 mobileRepoFactory,
                 carrierMergedRepoFactory,
@@ -173,7 +184,7 @@
             const val MOBILE_CONNECTION_BUFFER_SIZE = 100
 
             /** Returns a log buffer name for a mobile connection with the given [subId]. */
-            fun tableBufferLogName(subId: Int): String = "MobileConnectionLog [$subId]"
+            fun tableBufferLogName(subId: Int): String = "MobileConnectionLog[$subId]"
         }
     }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
index 3f2ce40..dcce0ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryImpl.kt
@@ -18,8 +18,6 @@
 
 import android.content.Context
 import android.content.IntentFilter
-import android.database.ContentObserver
-import android.provider.Settings.Global
 import android.telephony.CellSignalStrength
 import android.telephony.CellSignalStrengthCdma
 import android.telephony.ServiceState
@@ -38,20 +36,20 @@
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
 import com.android.systemui.log.table.TableLogBuffer
-import com.android.systemui.log.table.logDiffsForTable
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
 import com.android.systemui.statusbar.pipeline.mobile.data.model.toDataConnectionType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
 import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
-import com.android.systemui.util.settings.GlobalSettings
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -60,13 +58,15 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.flow.scan
+import kotlinx.coroutines.flow.shareIn
 import kotlinx.coroutines.flow.stateIn
 
 /**
@@ -81,19 +81,18 @@
     defaultNetworkName: NetworkNameModel,
     networkNameSeparator: String,
     private val telephonyManager: TelephonyManager,
-    private val globalSettings: GlobalSettings,
+    systemUiCarrierConfig: SystemUiCarrierConfig,
     broadcastDispatcher: BroadcastDispatcher,
-    globalMobileDataSettingChangedEvent: Flow<Unit>,
-    mobileMappingsProxy: MobileMappingsProxy,
+    private val mobileMappingsProxy: MobileMappingsProxy,
     bgDispatcher: CoroutineDispatcher,
     logger: ConnectivityPipelineLogger,
-    mobileLogger: TableLogBuffer,
+    override val tableLogBuffer: TableLogBuffer,
     scope: CoroutineScope,
 ) : MobileConnectionRepository {
     init {
         if (telephonyManager.subscriptionId != subId) {
             throw IllegalStateException(
-                "TelephonyManager should be created with subId($subId). " +
+                "MobileRepo: TelephonyManager should be created with subId($subId). " +
                     "Found ${telephonyManager.subscriptionId} instead."
             )
         }
@@ -101,10 +100,15 @@
 
     private val telephonyCallbackEvent = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
 
-    override val tableLogBuffer: TableLogBuffer = mobileLogger
-
-    override val connectionInfo: StateFlow<MobileConnectionModel> = run {
-        var state = MobileConnectionModel()
+    /**
+     * This flow defines the single shared connection to system_server via TelephonyCallback. Any
+     * new callback should be added to this listener and funneled through callbackEvents via a data
+     * class. See [CallbackEvent] for defining new callbacks.
+     *
+     * The reason we need to do this is because TelephonyManager limits the number of registered
+     * listeners per-process, so we don't want to create a new listener for every callback.
+     */
+    private val callbackEvents: SharedFlow<CallbackEvent> =
         conflatedCallbackFlow {
                 val callback =
                     object :
@@ -114,41 +118,16 @@
                         TelephonyCallback.DataConnectionStateListener,
                         TelephonyCallback.DataActivityListener,
                         TelephonyCallback.CarrierNetworkListener,
-                        TelephonyCallback.DisplayInfoListener {
+                        TelephonyCallback.DisplayInfoListener,
+                        TelephonyCallback.DataEnabledListener {
                         override fun onServiceStateChanged(serviceState: ServiceState) {
                             logger.logOnServiceStateChanged(serviceState, subId)
-                            state =
-                                state.copy(
-                                    isEmergencyOnly = serviceState.isEmergencyOnly,
-                                    isRoaming = serviceState.roaming,
-                                    operatorAlphaShort = serviceState.operatorAlphaShort,
-                                    isInService = Utils.isInService(serviceState),
-                                )
-                            trySend(state)
+                            trySend(CallbackEvent.OnServiceStateChanged(serviceState))
                         }
 
                         override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
                             logger.logOnSignalStrengthsChanged(signalStrength, subId)
-                            val cdmaLevel =
-                                signalStrength
-                                    .getCellSignalStrengths(CellSignalStrengthCdma::class.java)
-                                    .let { strengths ->
-                                        if (!strengths.isEmpty()) {
-                                            strengths[0].level
-                                        } else {
-                                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
-                                        }
-                                    }
-
-                            val primaryLevel = signalStrength.level
-
-                            state =
-                                state.copy(
-                                    cdmaLevel = cdmaLevel,
-                                    primaryLevel = primaryLevel,
-                                    isGsm = signalStrength.isGsm,
-                                )
-                            trySend(state)
+                            trySend(CallbackEvent.OnSignalStrengthChanged(signalStrength))
                         }
 
                         override fun onDataConnectionStateChanged(
@@ -156,101 +135,131 @@
                             networkType: Int
                         ) {
                             logger.logOnDataConnectionStateChanged(dataState, networkType, subId)
-                            state =
-                                state.copy(dataConnectionState = dataState.toDataConnectionType())
-                            trySend(state)
+                            trySend(CallbackEvent.OnDataConnectionStateChanged(dataState))
                         }
 
                         override fun onDataActivity(direction: Int) {
                             logger.logOnDataActivity(direction, subId)
-                            state =
-                                state.copy(
-                                    dataActivityDirection = direction.toMobileDataActivityModel()
-                                )
-                            trySend(state)
+                            trySend(CallbackEvent.OnDataActivity(direction))
                         }
 
                         override fun onCarrierNetworkChange(active: Boolean) {
                             logger.logOnCarrierNetworkChange(active, subId)
-                            state = state.copy(carrierNetworkChangeActive = active)
-                            trySend(state)
+                            trySend(CallbackEvent.OnCarrierNetworkChange(active))
                         }
 
                         override fun onDisplayInfoChanged(
                             telephonyDisplayInfo: TelephonyDisplayInfo
                         ) {
                             logger.logOnDisplayInfoChanged(telephonyDisplayInfo, subId)
+                            trySend(CallbackEvent.OnDisplayInfoChanged(telephonyDisplayInfo))
+                        }
 
-                            val networkType =
-                                if (telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) {
-                                    UnknownNetworkType
-                                } else if (
-                                    telephonyDisplayInfo.overrideNetworkType ==
-                                        OVERRIDE_NETWORK_TYPE_NONE
-                                ) {
-                                    DefaultNetworkType(
-                                        mobileMappingsProxy.toIconKey(
-                                            telephonyDisplayInfo.networkType
-                                        )
-                                    )
-                                } else {
-                                    OverrideNetworkType(
-                                        mobileMappingsProxy.toIconKeyOverride(
-                                            telephonyDisplayInfo.overrideNetworkType
-                                        )
-                                    )
-                                }
-                            state = state.copy(resolvedNetworkType = networkType)
-                            trySend(state)
+                        override fun onDataEnabledChanged(enabled: Boolean, reason: Int) {
+                            logger.logOnDataEnabledChanged(enabled, subId)
+                            trySend(CallbackEvent.OnDataEnabledChanged(enabled))
                         }
                     }
                 telephonyManager.registerTelephonyCallback(bgDispatcher.asExecutor(), callback)
                 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
             }
-            .onEach { telephonyCallbackEvent.tryEmit(Unit) }
-            .logDiffsForTable(
-                mobileLogger,
-                columnPrefix = "MobileConnection ($subId)",
-                initialValue = state,
-            )
-            .stateIn(scope, SharingStarted.WhileSubscribed(), state)
+            .shareIn(scope, SharingStarted.WhileSubscribed())
+
+    private fun updateConnectionState(
+        prevState: MobileConnectionModel,
+        callbackEvent: CallbackEvent,
+    ): MobileConnectionModel =
+        when (callbackEvent) {
+            is CallbackEvent.OnServiceStateChanged -> {
+                val serviceState = callbackEvent.serviceState
+                prevState.copy(
+                    isEmergencyOnly = serviceState.isEmergencyOnly,
+                    isRoaming = serviceState.roaming,
+                    operatorAlphaShort = serviceState.operatorAlphaShort,
+                    isInService = Utils.isInService(serviceState),
+                )
+            }
+            is CallbackEvent.OnSignalStrengthChanged -> {
+                val signalStrength = callbackEvent.signalStrength
+                val cdmaLevel =
+                    signalStrength.getCellSignalStrengths(CellSignalStrengthCdma::class.java).let {
+                        strengths ->
+                        if (!strengths.isEmpty()) {
+                            strengths[0].level
+                        } else {
+                            CellSignalStrength.SIGNAL_STRENGTH_NONE_OR_UNKNOWN
+                        }
+                    }
+
+                val primaryLevel = signalStrength.level
+
+                prevState.copy(
+                    cdmaLevel = cdmaLevel,
+                    primaryLevel = primaryLevel,
+                    isGsm = signalStrength.isGsm,
+                )
+            }
+            is CallbackEvent.OnDataConnectionStateChanged -> {
+                prevState.copy(dataConnectionState = callbackEvent.dataState.toDataConnectionType())
+            }
+            is CallbackEvent.OnDataActivity -> {
+                prevState.copy(
+                    dataActivityDirection = callbackEvent.direction.toMobileDataActivityModel()
+                )
+            }
+            is CallbackEvent.OnCarrierNetworkChange -> {
+                prevState.copy(carrierNetworkChangeActive = callbackEvent.active)
+            }
+            is CallbackEvent.OnDisplayInfoChanged -> {
+                val telephonyDisplayInfo = callbackEvent.telephonyDisplayInfo
+                val networkType =
+                    if (telephonyDisplayInfo.networkType == NETWORK_TYPE_UNKNOWN) {
+                        UnknownNetworkType
+                    } else if (
+                        telephonyDisplayInfo.overrideNetworkType == OVERRIDE_NETWORK_TYPE_NONE
+                    ) {
+                        DefaultNetworkType(
+                            mobileMappingsProxy.toIconKey(telephonyDisplayInfo.networkType)
+                        )
+                    } else {
+                        OverrideNetworkType(
+                            mobileMappingsProxy.toIconKeyOverride(
+                                telephonyDisplayInfo.overrideNetworkType
+                            )
+                        )
+                    }
+                prevState.copy(resolvedNetworkType = networkType)
+            }
+            is CallbackEvent.OnDataEnabledChanged -> {
+                // Not part of this object, handled in a separate flow
+                prevState
+            }
+        }
+
+    override val connectionInfo = run {
+        val initial = MobileConnectionModel()
+        callbackEvents
+            .scan(initial, ::updateConnectionState)
+            .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
     }
 
-    // This will become variable based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL]
-    // once it's wired up inside of [CarrierConfigTracker].
-    override val numberOfLevels: StateFlow<Int> =
-        flowOf(DEFAULT_NUM_LEVELS)
-            .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
-
-    /** Produces whenever the mobile data setting changes for this subId */
-    private val localMobileDataSettingChangedEvent: Flow<Unit> = conflatedCallbackFlow {
-        val observer =
-            object : ContentObserver(null) {
-                override fun onChange(selfChange: Boolean) {
-                    trySend(Unit)
+    override val numberOfLevels =
+        systemUiCarrierConfig.shouldInflateSignalStrength
+            .map { shouldInflate ->
+                if (shouldInflate) {
+                    DEFAULT_NUM_LEVELS + 1
+                } else {
+                    DEFAULT_NUM_LEVELS
                 }
             }
-
-        globalSettings.registerContentObserver(
-            globalSettings.getUriFor("${Global.MOBILE_DATA}$subId"),
-            /* notifyForDescendants */ true,
-            observer
-        )
-
-        awaitClose { context.contentResolver.unregisterContentObserver(observer) }
-    }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), DEFAULT_NUM_LEVELS)
 
     /**
      * There are a few cases where we will need to poll [TelephonyManager] so we can update some
      * internal state where callbacks aren't provided. Any of those events should be merged into
      * this flow, which can be used to trigger the polling.
      */
-    private val telephonyPollingEvent: Flow<Unit> =
-        merge(
-            telephonyCallbackEvent,
-            localMobileDataSettingChangedEvent,
-            globalMobileDataSettingChangedEvent,
-        )
+    private val telephonyPollingEvent: Flow<Unit> = callbackEvents.map { Unit }
 
     override val cdmaRoaming: StateFlow<Boolean> =
         telephonyPollingEvent
@@ -259,39 +268,23 @@
 
     override val networkName: StateFlow<NetworkNameModel> =
         broadcastDispatcher
-            .broadcastFlow(IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED)) {
-                intent,
-                _ ->
-                if (intent.getIntExtra(EXTRA_SUBSCRIPTION_ID, INVALID_SUBSCRIPTION_ID) != subId) {
-                    defaultNetworkName
-                } else {
-                    intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName
-                }
-            }
-            .distinctUntilChanged()
-            .logDiffsForTable(
-                mobileLogger,
-                columnPrefix = "",
-                initialValue = defaultNetworkName,
+            .broadcastFlow(
+                filter = IntentFilter(TelephonyManager.ACTION_SERVICE_PROVIDERS_UPDATED),
+                map = { intent, _ -> intent },
             )
+            .filter { intent ->
+                intent.getIntExtra(EXTRA_SUBSCRIPTION_ID, INVALID_SUBSCRIPTION_ID) == subId
+            }
+            .map { intent -> intent.toNetworkNameModel(networkNameSeparator) ?: defaultNetworkName }
             .stateIn(scope, SharingStarted.WhileSubscribed(), defaultNetworkName)
 
-    override val dataEnabled: StateFlow<Boolean> = run {
-        val initial = dataConnectionAllowed()
-        telephonyPollingEvent
-            .mapLatest { dataConnectionAllowed() }
-            .distinctUntilChanged()
-            .logDiffsForTable(
-                mobileLogger,
-                columnPrefix = "",
-                columnName = "dataEnabled",
-                initialValue = initial,
-            )
+    override val dataEnabled = run {
+        val initial = telephonyManager.isDataConnectionAllowed
+        callbackEvents
+            .mapNotNull { (it as? CallbackEvent.OnDataEnabledChanged)?.enabled }
             .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
     }
 
-    private fun dataConnectionAllowed(): Boolean = telephonyManager.isDataConnectionAllowed
-
     class Factory
     @Inject
     constructor(
@@ -299,7 +292,7 @@
         private val context: Context,
         private val telephonyManager: TelephonyManager,
         private val logger: ConnectivityPipelineLogger,
-        private val globalSettings: GlobalSettings,
+        private val carrierConfigRepository: CarrierConfigRepository,
         private val mobileMappingsProxy: MobileMappingsProxy,
         @Background private val bgDispatcher: CoroutineDispatcher,
         @Application private val scope: CoroutineScope,
@@ -309,7 +302,6 @@
             mobileLogger: TableLogBuffer,
             defaultNetworkName: NetworkNameModel,
             networkNameSeparator: String,
-            globalMobileDataSettingChangedEvent: Flow<Unit>,
         ): MobileConnectionRepository {
             return MobileConnectionRepositoryImpl(
                 context,
@@ -317,9 +309,8 @@
                 defaultNetworkName,
                 networkNameSeparator,
                 telephonyManager.createForSubscriptionId(subId),
-                globalSettings,
+                carrierConfigRepository.getOrCreateConfigForSubId(subId),
                 broadcastDispatcher,
-                globalMobileDataSettingChangedEvent,
                 mobileMappingsProxy,
                 bgDispatcher,
                 logger,
@@ -329,3 +320,17 @@
         }
     }
 }
+
+/**
+ * Wrap every [TelephonyCallback] we care about in a data class so we can accept them in a single
+ * shared flow and then split them back out into other flows.
+ */
+private sealed interface CallbackEvent {
+    data class OnServiceStateChanged(val serviceState: ServiceState) : CallbackEvent
+    data class OnSignalStrengthChanged(val signalStrength: SignalStrength) : CallbackEvent
+    data class OnDataConnectionStateChanged(val dataState: Int) : CallbackEvent
+    data class OnDataActivity(val direction: Int) : CallbackEvent
+    data class OnCarrierNetworkChange(val active: Boolean) : CallbackEvent
+    data class OnDisplayInfoChanged(val telephonyDisplayInfo: TelephonyDisplayInfo) : CallbackEvent
+    data class OnDataEnabledChanged(val enabled: Boolean) : CallbackEvent
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
index 39ad31f..c660d31 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryImpl.kt
@@ -19,14 +19,12 @@
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.IntentFilter
-import android.database.ContentObserver
 import android.net.ConnectivityManager
 import android.net.ConnectivityManager.NetworkCallback
 import android.net.Network
 import android.net.NetworkCapabilities
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
-import android.provider.Settings.Global.MOBILE_DATA
 import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
@@ -44,6 +42,9 @@
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -54,7 +55,6 @@
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.WifiRepository
 import com.android.systemui.util.kotlin.pairwise
-import com.android.systemui.util.settings.GlobalSettings
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineDispatcher
 import kotlinx.coroutines.CoroutineScope
@@ -85,9 +85,9 @@
     private val subscriptionManager: SubscriptionManager,
     private val telephonyManager: TelephonyManager,
     private val logger: ConnectivityPipelineLogger,
+    @MobileSummaryLog private val tableLogger: TableLogBuffer,
     mobileMappingsProxy: MobileMappingsProxy,
     broadcastDispatcher: BroadcastDispatcher,
-    private val globalSettings: GlobalSettings,
     private val context: Context,
     @Background private val bgDispatcher: CoroutineDispatcher,
     @Application private val scope: CoroutineScope,
@@ -118,6 +118,12 @@
                 }
             }
             .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "carrierMergedSubId",
+                initialValue = null,
+            )
             .stateIn(scope, started = SharingStarted.WhileSubscribed(), null)
 
     private val mobileSubscriptionsChangeEvent: Flow<Unit> = conflatedCallbackFlow {
@@ -143,8 +149,14 @@
     override val subscriptions: StateFlow<List<SubscriptionModel>> =
         merge(mobileSubscriptionsChangeEvent, carrierMergedSubId)
             .mapLatest { fetchSubscriptionsList().map { it.toSubscriptionModel() } }
-            .logInputChange(logger, "onSubscriptionsChanged")
             .onEach { infos -> updateRepos(infos) }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "subscriptions",
+                initialValue = listOf(),
+            )
             .stateIn(scope, started = SharingStarted.WhileSubscribed(), listOf())
 
     /** StateFlow that keeps track of the current active mobile data subscription */
@@ -161,7 +173,12 @@
                 awaitClose { telephonyManager.unregisterTelephonyCallback(callback) }
             }
             .distinctUntilChanged()
-            .logInputChange(logger, "onActiveDataSubscriptionIdChanged")
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "activeSubId",
+                initialValue = INVALID_SUBSCRIPTION_ID,
+            )
             .stateIn(scope, started = SharingStarted.WhileSubscribed(), INVALID_SUBSCRIPTION_ID)
 
     private val defaultDataSubIdChangeEvent: MutableSharedFlow<Unit> =
@@ -175,7 +192,12 @@
                 intent.getIntExtra(PhoneConstants.SUBSCRIPTION_KEY, INVALID_SUBSCRIPTION_ID)
             }
             .distinctUntilChanged()
-            .logInputChange(logger, "ACTION_DEFAULT_DATA_SUBSCRIPTION_CHANGED")
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "defaultSubId",
+                initialValue = SubscriptionManager.getDefaultDataSubscriptionId(),
+            )
             .onEach { defaultDataSubIdChangeEvent.tryEmit(Unit) }
             .stateIn(
                 scope,
@@ -222,29 +244,6 @@
             ?: createRepositoryForSubId(subId).also { subIdRepositoryCache[subId] = it }
     }
 
-    /**
-     * In single-SIM devices, the [MOBILE_DATA] setting is phone-wide. For multi-SIM, the individual
-     * connection repositories also observe the URI for [MOBILE_DATA] + subId.
-     */
-    override val globalMobileDataSettingChangedEvent: Flow<Unit> =
-        conflatedCallbackFlow {
-                val observer =
-                    object : ContentObserver(null) {
-                        override fun onChange(selfChange: Boolean) {
-                            trySend(Unit)
-                        }
-                    }
-
-                globalSettings.registerContentObserver(
-                    globalSettings.getUriFor(MOBILE_DATA),
-                    true,
-                    observer
-                )
-
-                awaitClose { context.contentResolver.unregisterContentObserver(observer) }
-            }
-            .logInputChange(logger, "globalMobileDataSettingChangedEvent")
-
     @SuppressLint("MissingPermission")
     override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
         conflatedCallbackFlow {
@@ -274,7 +273,11 @@
                 awaitClose { connectivityManager.unregisterNetworkCallback(callback) }
             }
             .distinctUntilChanged()
-            .logInputChange(logger, "defaultMobileNetworkConnectivity")
+            .logDiffsForTable(
+                tableLogger,
+                columnPrefix = "$LOGGING_PREFIX.defaultConnection",
+                initialValue = MobileConnectivityModel(),
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), MobileConnectivityModel())
 
     /**
@@ -315,7 +318,6 @@
             isCarrierMerged(subId),
             defaultNetworkName,
             networkNameSeparator,
-            globalMobileDataSettingChangedEvent,
         )
     }
 
@@ -349,4 +351,8 @@
             subscriptionId = subscriptionId,
             isOpportunistic = isOpportunistic,
         )
+
+    companion object {
+        private const val LOGGING_PREFIX = "Repo"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
index 9cdff96..7b0f952 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt
@@ -33,7 +33,9 @@
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
 interface MobileIconInteractor {
@@ -109,6 +111,9 @@
 
     /** Based on [CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL], either 4 or 5 */
     val numberOfLevels: StateFlow<Int>
+
+    /** See [MobileIconsInteractor.isForceHidden]. */
+    val isForceHidden: Flow<Boolean>
 }
 
 /** Interactor for a single mobile connection. This connection _should_ have one subscription ID */
@@ -124,6 +129,7 @@
     defaultMobileIconGroup: StateFlow<MobileIconGroup>,
     defaultDataSubId: StateFlow<Int>,
     override val isDefaultConnectionFailed: StateFlow<Boolean>,
+    override val isForceHidden: Flow<Boolean>,
     connectionRepository: MobileConnectionRepository,
 ) : MobileIconInteractor {
     private val connectionInfo = connectionRepository.connectionInfo
@@ -152,7 +158,7 @@
                 if (
                     networkName is NetworkNameModel.Default && connection.operatorAlphaShort != null
                 ) {
-                    NetworkNameModel.Derived(connection.operatorAlphaShort)
+                    NetworkNameModel.IntentDerived(connection.operatorAlphaShort)
                 } else {
                     networkName
                 }
@@ -181,6 +187,16 @@
                     else -> mapping[info.resolvedNetworkType.lookupKey] ?: defaultGroup
                 }
             }
+            .distinctUntilChanged()
+            .onEach {
+                // Doesn't use [logDiffsForTable] because [MobileIconGroup] can't implement the
+                // [Diffable] interface.
+                tableLogBuffer.logChange(
+                    prefix = "",
+                    columnName = "networkTypeIcon",
+                    value = it.name
+                )
+            }
             .stateIn(scope, SharingStarted.WhileSubscribed(), defaultMobileIconGroup.value)
 
     override val isEmergencyOnly: StateFlow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
index 0e4a432..94f7af4 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt
@@ -23,12 +23,17 @@
 import com.android.settingslib.mobile.TelephonyIcons
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.log.table.TableLogBuffer
+import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository
 import com.android.systemui.util.CarrierConfigTracker
 import javax.inject.Inject
 import kotlinx.coroutines.CoroutineScope
@@ -42,8 +47,8 @@
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.transformLatest
 
@@ -88,6 +93,10 @@
     val isDefaultConnectionFailed: StateFlow<Boolean>
     /** True once the user has been set up */
     val isUserSetup: StateFlow<Boolean>
+
+    /** True if we're configured to force-hide the mobile icons and false otherwise. */
+    val isForceHidden: Flow<Boolean>
+
     /**
      * Vends out a [MobileIconInteractor] tracking the [MobileConnectionRepository] for the given
      * subId. Will throw if the ID is invalid
@@ -104,6 +113,8 @@
     private val mobileConnectionsRepo: MobileConnectionsRepository,
     private val carrierConfigTracker: CarrierConfigTracker,
     private val logger: ConnectivityPipelineLogger,
+    @MobileSummaryLog private val tableLogger: TableLogBuffer,
+    connectivityRepository: ConnectivityRepository,
     userSetupRepo: UserSetupRepository,
     @Application private val scope: CoroutineScope,
 ) : MobileIconsInteractor {
@@ -173,7 +184,13 @@
             }
         }
             .distinctUntilChanged()
-            .onEach { logger.logFilteredSubscriptionsChanged(it) }
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "filteredSubscriptions",
+                initialValue = listOf(),
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), listOf())
 
     override val defaultDataSubId = mobileConnectionsRepo.defaultDataSubId
 
@@ -195,6 +212,12 @@
                 delay(2000)
                 emit(false)
             }
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "forcingValidation",
+                initialValue = false,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val defaultMobileNetworkConnectivity: StateFlow<MobileConnectivityModel> =
@@ -211,6 +234,12 @@
                     networkConnectivity
                 }
             }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                tableLogger,
+                columnPrefix = "$LOGGING_PREFIX.defaultConnection",
+                initialValue = mobileConnectionsRepo.defaultMobileNetworkConnectivity.value,
+            )
             .stateIn(
                 scope,
                 SharingStarted.WhileSubscribed(),
@@ -259,10 +288,21 @@
                     !connectivityModel.isValidated
                 }
             }
+            .logDiffsForTable(
+                tableLogger,
+                LOGGING_PREFIX,
+                columnName = "isDefaultConnectionFailed",
+                initialValue = false,
+            )
             .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val isUserSetup: StateFlow<Boolean> = userSetupRepo.isUserSetupFlow
 
+    override val isForceHidden: Flow<Boolean> =
+        connectivityRepository.forceHiddenSlots
+            .map { it.contains(ConnectivitySlot.MOBILE) }
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
     /** Vends out new [MobileIconInteractor] for a particular subId */
     override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor =
         MobileIconInteractorImpl(
@@ -275,6 +315,11 @@
             defaultMobileIconGroup,
             defaultDataSubId,
             isDefaultConnectionFailed,
+            isForceHidden,
             mobileConnectionsRepo.getRepoForSubId(subId),
         )
+
+    companion object {
+        private const val LOGGING_PREFIX = "Intr"
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
index a4b2abc..db585e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt
@@ -91,10 +91,17 @@
                     }
                 }
 
+                launch { viewModel.isVisible.collect { isVisible -> view.isVisible = isVisible } }
+
                 // Set the icon for the triangle
                 launch {
-                    viewModel.iconId.distinctUntilChanged().collect { iconId ->
-                        mobileDrawable.level = iconId
+                    viewModel.icon.distinctUntilChanged().collect { icon ->
+                        mobileDrawable.level =
+                            SignalDrawable.getState(
+                                icon.level,
+                                icon.numberOfLevels,
+                                icon.showExclamationMark,
+                            )
                     }
                 }
 
@@ -148,8 +155,7 @@
 
         return object : ModernStatusBarViewBinding {
             override fun getShouldIconBeVisible(): Boolean {
-                // If this view model exists, then the icon should be visible.
-                return true
+                return viewModel.isVisible.value
             }
 
             override fun onVisibilityStateChanged(@StatusBarIconView.VisibleState state: Int) {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModel.kt
new file mode 100644
index 0000000..16e1766
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModel.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 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.mobile.ui.model
+
+import com.android.systemui.log.table.Diffable
+import com.android.systemui.log.table.TableRowLogger
+
+/** A model that will be consumed by [SignalDrawable] to show the mobile triangle icon. */
+data class SignalIconModel(
+    val level: Int,
+    val numberOfLevels: Int,
+    val showExclamationMark: Boolean,
+) : Diffable<SignalIconModel> {
+    // TODO(b/267767715): Can we implement [logDiffs] and [logFull] generically for data classes?
+    override fun logDiffs(prevVal: SignalIconModel, row: TableRowLogger) {
+        if (prevVal.level != level) {
+            row.logChange(COL_LEVEL, level)
+        }
+        if (prevVal.numberOfLevels != numberOfLevels) {
+            row.logChange(COL_NUM_LEVELS, numberOfLevels)
+        }
+        if (prevVal.showExclamationMark != showExclamationMark) {
+            row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark)
+        }
+    }
+
+    override fun logFull(row: TableRowLogger) {
+        row.logChange(COL_LEVEL, level)
+        row.logChange(COL_NUM_LEVELS, numberOfLevels)
+        row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark)
+    }
+
+    companion object {
+        /** Creates a [SignalIconModel] representing an empty and invalidated state. */
+        fun createEmptyState(numberOfLevels: Int) =
+            SignalIconModel(level = 0, numberOfLevels, showExclamationMark = true)
+
+        private const val COL_LEVEL = "level"
+        private const val COL_NUM_LEVELS = "numLevels"
+        private const val COL_SHOW_EXCLAMATION = "showExclamation"
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
index 9e2024a..0496278 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt
@@ -22,10 +22,11 @@
 import com.android.systemui.common.shared.model.ContentDescription
 import com.android.systemui.common.shared.model.Icon
 import com.android.systemui.log.table.logDiffsForTable
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -37,14 +38,14 @@
 import kotlinx.coroutines.flow.flowOf
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.mapLatest
-import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.stateIn
 
 /** Common interface for all of the location-based mobile icon view models. */
 interface MobileIconViewModelCommon {
     val subscriptionId: Int
-    /** An int consumable by [SignalDrawable] for display */
-    val iconId: Flow<Int>
+    /** True if this view should be visible at all. */
+    val isVisible: StateFlow<Boolean>
+    val icon: Flow<SignalIconModel>
     val contentDescription: Flow<ContentDescription>
     val roaming: Flow<Boolean>
     /** The RAT icon (LTE, 3G, 5G, etc) to be displayed. Null if we shouldn't show anything */
@@ -73,7 +74,7 @@
 constructor(
     override val subscriptionId: Int,
     iconInteractor: MobileIconInteractor,
-    logger: ConnectivityPipelineLogger,
+    airplaneModeInteractor: AirplaneModeInteractor,
     constants: ConnectivityConstants,
     scope: CoroutineScope,
 ) : MobileIconViewModelCommon {
@@ -81,8 +82,28 @@
     private val showExclamationMark: Flow<Boolean> =
         iconInteractor.isDefaultDataEnabled.mapLatest { !it }
 
-    override val iconId: Flow<Int> = run {
-        val initial = SignalDrawable.getEmptyState(iconInteractor.numberOfLevels.value)
+    override val isVisible: StateFlow<Boolean> =
+        if (!constants.hasDataCapabilities) {
+                flowOf(false)
+            } else {
+                combine(
+                    airplaneModeInteractor.isAirplaneMode,
+                    iconInteractor.isForceHidden,
+                ) { isAirplaneMode, isForceHidden ->
+                    !isAirplaneMode && !isForceHidden
+                }
+            }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                iconInteractor.tableLogBuffer,
+                columnPrefix = "",
+                columnName = "visible",
+                initialValue = false,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
+
+    override val icon: Flow<SignalIconModel> = run {
+        val initial = SignalIconModel.createEmptyState(iconInteractor.numberOfLevels.value)
         combine(
                 iconInteractor.level,
                 iconInteractor.numberOfLevels,
@@ -90,16 +111,15 @@
                 iconInteractor.isInService,
             ) { level, numberOfLevels, showExclamationMark, isInService ->
                 if (!isInService) {
-                    SignalDrawable.getEmptyState(numberOfLevels)
+                    SignalIconModel.createEmptyState(numberOfLevels)
                 } else {
-                    SignalDrawable.getState(level, numberOfLevels, showExclamationMark)
+                    SignalIconModel(level, numberOfLevels, showExclamationMark)
                 }
             }
             .distinctUntilChanged()
             .logDiffsForTable(
                 iconInteractor.tableLogBuffer,
-                columnPrefix = "",
-                columnName = "iconId",
+                columnPrefix = "icon",
                 initialValue = initial,
             )
             .stateIn(scope, SharingStarted.WhileSubscribed(), initial)
@@ -124,14 +144,22 @@
 
     private val showNetworkTypeIcon: Flow<Boolean> =
         combine(
-            iconInteractor.isDataConnected,
-            iconInteractor.isDataEnabled,
-            iconInteractor.isDefaultConnectionFailed,
-            iconInteractor.alwaysShowDataRatIcon,
-            iconInteractor.isConnected,
-        ) { dataConnected, dataEnabled, failedConnection, alwaysShow, connected ->
-            alwaysShow || (dataConnected && dataEnabled && !failedConnection && connected)
-        }
+                iconInteractor.isDataConnected,
+                iconInteractor.isDataEnabled,
+                iconInteractor.isDefaultConnectionFailed,
+                iconInteractor.alwaysShowDataRatIcon,
+                iconInteractor.isConnected,
+            ) { dataConnected, dataEnabled, failedConnection, alwaysShow, connected ->
+                alwaysShow || (dataConnected && dataEnabled && !failedConnection && connected)
+            }
+            .distinctUntilChanged()
+            .logDiffsForTable(
+                iconInteractor.tableLogBuffer,
+                columnPrefix = "",
+                columnName = "showNetworkTypeIcon",
+                initialValue = false,
+            )
+            .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     override val networkTypeIcon: Flow<Icon?> =
         combine(
@@ -149,14 +177,6 @@
                 }
             }
             .distinctUntilChanged()
-            .onEach {
-                // This is done as an onEach side effect since Icon is not Diffable (yet)
-                iconInteractor.tableLogBuffer.logChange(
-                    prefix = "",
-                    columnName = "networkTypeIcon",
-                    value = it.toString(),
-                )
-            }
             .stateIn(scope, SharingStarted.WhileSubscribed(), null)
 
     override val roaming: StateFlow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
index 24370d2..185b668 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt
@@ -17,9 +17,11 @@
 package com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel
 
 import androidx.annotation.VisibleForTesting
+import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.dagger.qualifiers.Application
 import com.android.systemui.statusbar.phone.StatusBarLocation
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.view.ModernStatusBarMobileView
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
@@ -39,6 +41,7 @@
 constructor(
     val subscriptionIdsFlow: StateFlow<List<Int>>,
     private val interactor: MobileIconsInteractor,
+    private val airplaneModeInteractor: AirplaneModeInteractor,
     private val logger: ConnectivityPipelineLogger,
     private val constants: ConnectivityConstants,
     @Application private val scope: CoroutineScope,
@@ -56,7 +59,7 @@
                 ?: MobileIconViewModel(
                         subId,
                         interactor.createMobileConnectionInteractorForSubId(subId),
-                        logger,
+                        airplaneModeInteractor,
                         constants,
                         scope,
                     )
@@ -74,10 +77,12 @@
         subIdsToRemove.forEach { mobileIconSubIdCache.remove(it) }
     }
 
+    @SysUISingleton
     class Factory
     @Inject
     constructor(
         private val interactor: MobileIconsInteractor,
+        private val airplaneModeInteractor: AirplaneModeInteractor,
         private val logger: ConnectivityPipelineLogger,
         private val constants: ConnectivityConstants,
         @Application private val scope: CoroutineScope,
@@ -87,6 +92,7 @@
             return MobileIconsViewModel(
                 subscriptionIdsFlow,
                 interactor,
+                airplaneModeInteractor,
                 logger,
                 constants,
                 scope,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
index 7c7ffaf..45036969 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ConnectivityPipelineLogger.kt
@@ -204,15 +204,6 @@
 
     // TODO(b/238425913): We should split this class into mobile-specific and wifi-specific loggers.
 
-    fun logFilteredSubscriptionsChanged(subs: List<SubscriptionModel>) {
-        buffer.log(
-            SB_LOGGING_TAG,
-            LogLevel.INFO,
-            { str1 = subs.toString() },
-            { "Filtered subscriptions updated: $str1" },
-        )
-    }
-
     fun logUiAdapterSubIdsUpdated(subs: List<Int>) {
         buffer.log(
             SB_LOGGING_TAG,
@@ -231,6 +222,27 @@
         )
     }
 
+    fun logCarrierConfigChanged(subId: Int) {
+        buffer.log(
+            SB_LOGGING_TAG,
+            LogLevel.INFO,
+            { int1 = subId },
+            { "onCarrierConfigChanged: subId=$int1" },
+        )
+    }
+
+    fun logOnDataEnabledChanged(enabled: Boolean, subId: Int) {
+        buffer.log(
+            SB_LOGGING_TAG,
+            LogLevel.INFO,
+            {
+                int1 = subId
+                bool1 = enabled
+            },
+            { "onDataEnabledChanged: subId=$int1 enabled=$bool1" },
+        )
+    }
+
     companion object {
         const val SB_LOGGING_TAG = "SbConnectivity"
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
index caac8fa..7d2501ca 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoModeWifiDataSource.kt
@@ -53,7 +53,7 @@
 
     private fun Bundle.activeWifiEvent(): FakeWifiEventModel.Wifi {
         val level = getString("level")?.toInt()
-        val activity = getString("activity")?.toActivity()
+        val activity = getString("activity").toActivity()
         val ssid = getString("ssid")
         val validated = getString("fully").toBoolean()
 
@@ -69,11 +69,12 @@
         val subId = getString("slot")?.toInt() ?: DEFAULT_CARRIER_MERGED_SUB_ID
         val level = getString("level")?.toInt() ?: 0
         val numberOfLevels = getString("numlevels")?.toInt() ?: DEFAULT_NUM_LEVELS
+        val activity = getString("activity").toActivity()
 
-        return FakeWifiEventModel.CarrierMerged(subId, level, numberOfLevels)
+        return FakeWifiEventModel.CarrierMerged(subId, level, numberOfLevels, activity)
     }
 
-    private fun String.toActivity(): Int =
+    private fun String?.toActivity(): Int =
         when (this) {
             "inout" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_INOUT
             "in" -> WifiManager.TrafficStateCallback.DATA_ACTIVITY_IN
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
index e161b3e..a19c3c3e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/DemoWifiRepository.kt
@@ -80,17 +80,14 @@
     private fun processEnabledWifiState(event: FakeWifiEventModel.Wifi) {
         _isWifiEnabled.value = true
         _isWifiDefault.value = true
-        _wifiActivity.value =
-            event.activity?.toWifiDataActivityModel()
-                ?: DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+        _wifiActivity.value = event.activity.toWifiDataActivityModel()
         _wifiNetwork.value = event.toWifiNetworkModel()
     }
 
     private fun processCarrierMergedWifiState(event: FakeWifiEventModel.CarrierMerged) {
         _isWifiEnabled.value = true
         _isWifiDefault.value = true
-        // TODO(b/238425913): Support activity in demo mode.
-        _wifiActivity.value = DataActivityModel(hasActivityIn = false, hasActivityOut = false)
+        _wifiActivity.value = event.activity.toWifiDataActivityModel()
         _wifiNetwork.value = event.toCarrierMergedModel()
     }
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
index 518f8ce..f5035cbc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/demo/model/FakeWifiEventModel.kt
@@ -16,6 +16,8 @@
 
 package com.android.systemui.statusbar.pipeline.wifi.data.repository.demo.model
 
+import android.telephony.Annotation
+
 /**
  * Model for demo wifi commands, ported from [NetworkControllerImpl]
  *
@@ -24,7 +26,7 @@
 sealed interface FakeWifiEventModel {
     data class Wifi(
         val level: Int?,
-        val activity: Int?,
+        @Annotation.DataActivityType val activity: Int,
         val ssid: String?,
         val validated: Boolean?,
     ) : FakeWifiEventModel
@@ -33,6 +35,7 @@
         val subscriptionId: Int,
         val level: Int,
         val numberOfLevels: Int,
+        @Annotation.DataActivityType val activity: Int,
     ) : FakeWifiEventModel
 
     object WifiDisabled : FakeWifiEventModel
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt
index e2bebbe..f0949ac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsController.kt
@@ -25,6 +25,8 @@
          * If controls become available, initiate this callback with the desired position
          */
         fun onControlsUpdate(position: Int?)
+
+        fun removeControlsAutoTracker()
     }
 
     /** Add callback, supporting only a single callback at once */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
index 341eb3b..4950482 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImpl.kt
@@ -21,16 +21,15 @@
 import android.content.SharedPreferences
 import android.provider.Settings
 import android.util.Log
-
 import com.android.systemui.R
 import com.android.systemui.controls.ControlsServiceInfo
 import com.android.systemui.controls.dagger.ControlsComponent
 import com.android.systemui.controls.management.ControlsListingController
 import com.android.systemui.dagger.SysUISingleton
 import com.android.systemui.settings.UserContextProvider
+import com.android.systemui.statusbar.phone.AutoTileManager
 import com.android.systemui.statusbar.policy.DeviceControlsController.Callback
 import com.android.systemui.util.settings.SecureSettings
-
 import javax.inject.Inject
 
 /**
@@ -87,6 +86,10 @@
      * incorrect.
      */
     override fun setCallback(callback: Callback) {
+        if (!controlsComponent.isEnabled()) {
+            callback.removeControlsAutoTracker()
+            return
+        }
         // Treat any additional call as a reset before recalculating
         removeCallback()
         this.callback = callback
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerExt.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerExt.kt
new file mode 100644
index 0000000..5e36750
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/HeadsUpManagerExt.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.policy
+
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+
+/**
+ * Returns a [Flow] that emits events whenever a [NotificationEntry] enters or exists the "heads up"
+ * state.
+ */
+val HeadsUpManager.headsUpEvents: Flow<Pair<NotificationEntry, Boolean>>
+    get() = conflatedCallbackFlow {
+        val listener =
+            object : OnHeadsUpChangedListener {
+                override fun onHeadsUpStateChanged(entry: NotificationEntry, isHeadsUp: Boolean) {
+                    trySend(entry to isHeadsUp)
+                }
+            }
+        addListener(listener)
+        awaitClose { removeListener(listener) }
+    }
diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
index e5ab473..ad1e5fe 100644
--- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt
@@ -47,12 +47,14 @@
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.filterNotNull
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.runBlocking
 import kotlinx.coroutines.withContext
@@ -120,9 +122,25 @@
     featureFlags: FeatureFlags,
 ) : UserRepository {
 
-    private val _userSwitcherSettings = MutableStateFlow(runBlocking { getSettings() })
-    override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> =
-        _userSwitcherSettings.asStateFlow().filterNotNull()
+    private val _userSwitcherSettings: StateFlow<UserSwitcherSettingsModel> =
+        globalSettings
+            .observerFlow(
+                names =
+                    arrayOf(
+                        SETTING_SIMPLE_USER_SWITCHER,
+                        Settings.Global.ADD_USERS_WHEN_LOCKED,
+                        Settings.Global.USER_SWITCHER_ENABLED,
+                    ),
+                userId = UserHandle.USER_SYSTEM,
+            )
+            .onStart { emit(Unit) } // Forces an initial update.
+            .map { getSettings() }
+            .stateIn(
+                scope = applicationScope,
+                started = SharingStarted.Eagerly,
+                initialValue = runBlocking { getSettings() },
+            )
+    override val userSwitcherSettings: Flow<UserSwitcherSettingsModel> = _userSwitcherSettings
 
     private val _userInfos = MutableStateFlow<List<UserInfo>?>(null)
     override val userInfos: Flow<List<UserInfo>> = _userInfos.filterNotNull()
@@ -154,7 +172,6 @@
 
     init {
         observeSelectedUser()
-        observeUserSettings()
         if (featureFlags.isEnabled(FACE_AUTH_REFACTOR)) {
             observeUserSwitching()
         }
@@ -237,23 +254,6 @@
             .launchIn(applicationScope)
     }
 
-    private fun observeUserSettings() {
-        globalSettings
-            .observerFlow(
-                names =
-                    arrayOf(
-                        SETTING_SIMPLE_USER_SWITCHER,
-                        Settings.Global.ADD_USERS_WHEN_LOCKED,
-                        Settings.Global.USER_SWITCHER_ENABLED,
-                    ),
-                userId = UserHandle.USER_SYSTEM,
-            )
-            .onStart { emit(Unit) } // Forces an initial update.
-            .map { getSettings() }
-            .onEach { _userSwitcherSettings.value = it }
-            .launchIn(applicationScope)
-    }
-
     private suspend fun getSettings(): UserSwitcherSettingsModel {
         return withContext(backgroundDispatcher) {
             val isSimpleUserSwitcher =
diff --git a/packages/SystemUI/src/com/android/systemui/util/condition/ConditionalCoreStartable.java b/packages/SystemUI/src/com/android/systemui/util/condition/ConditionalCoreStartable.java
new file mode 100644
index 0000000..b41bca0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/condition/ConditionalCoreStartable.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.condition;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.shared.condition.Condition;
+import com.android.systemui.shared.condition.Monitor;
+
+import java.util.Set;
+
+/**
+ * {@link ConditionalCoreStartable} is a {@link com.android.systemui.CoreStartable} abstract
+ * implementation where conditions must be met before routines are executed.
+ */
+public abstract class ConditionalCoreStartable implements CoreStartable {
+    private final Monitor mMonitor;
+    private final Set<Condition> mConditionSet;
+    private Monitor.Subscription.Token mStartToken;
+    private Monitor.Subscription.Token mBootCompletedToken;
+
+    public ConditionalCoreStartable(Monitor monitor) {
+        this(monitor, null);
+    }
+
+    public ConditionalCoreStartable(Monitor monitor, Set<Condition> conditionSet) {
+        mMonitor = monitor;
+        mConditionSet = conditionSet;
+    }
+
+    @Override
+    public final void start() {
+        if (mConditionSet == null || mConditionSet.isEmpty()) {
+            onStart();
+            return;
+        }
+
+        mStartToken = mMonitor.addSubscription(
+                new Monitor.Subscription.Builder(allConditionsMet -> {
+                    if (allConditionsMet) {
+                        mMonitor.removeSubscription(mStartToken);
+                        mStartToken = null;
+                        onStart();
+                    }
+                }).addConditions(mConditionSet)
+                        .build());
+    }
+
+    protected abstract void onStart();
+
+    @Override
+    public final void onBootCompleted() {
+        if (mConditionSet == null || mConditionSet.isEmpty()) {
+            bootCompleted();
+            return;
+        }
+
+        mBootCompletedToken = mMonitor.addSubscription(
+                new Monitor.Subscription.Builder(allConditionsMet -> {
+                    if (allConditionsMet) {
+                        mMonitor.removeSubscription(mBootCompletedToken);
+                        mBootCompletedToken = null;
+                        bootCompleted();
+                    }
+                }).addConditions(mConditionSet)
+                        .build());
+    }
+
+    protected void bootCompleted() {
+    }
+}
diff --git a/packages/SystemUI/tests/res/layout/invalid_notification_height.xml b/packages/SystemUI/tests/res/layout/invalid_notification_height.xml
new file mode 100644
index 0000000..aac43bf
--- /dev/null
+++ b/packages/SystemUI/tests/res/layout/invalid_notification_height.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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.
+-->
+<View xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="5dp"/>
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 43a2017..f7fec80 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -58,7 +58,7 @@
 import org.mockito.Mockito.times
 import org.mockito.Mockito.verify
 import org.mockito.junit.MockitoJUnit
-import java.util.*
+import java.util.TimeZone
 import java.util.concurrent.Executor
 import org.mockito.Mockito.`when` as whenever
 
@@ -105,7 +105,9 @@
         repository = FakeKeyguardRepository()
 
         underTest = ClockEventController(
-            KeyguardInteractor(repository = repository, commandQueue = commandQueue),
+            KeyguardInteractor(repository = repository,
+                    commandQueue = commandQueue,
+                    featureFlags = featureFlags),
             KeyguardTransitionInteractor(repository = transitionRepository),
             broadcastDispatcher,
             batteryController,
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
index cd8857a..853d8ed 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java
@@ -27,6 +27,7 @@
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT;
 import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
 import static com.android.keyguard.FaceAuthApiRequestReason.NOTIFICATION_PANEL_CLICKED;
+import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_AVAILABLE;
 import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_STATE_CANCELLING_RESTARTING;
 import static com.android.keyguard.KeyguardUpdateMonitor.DEFAULT_CANCEL_SIGNAL_TIMEOUT;
 import static com.android.keyguard.KeyguardUpdateMonitor.HAL_POWER_PRESS_TIMEOUT;
@@ -240,7 +241,7 @@
     @Mock
     private FingerprintInteractiveToAuthProvider mInteractiveToAuthProvider;
 
-
+    private List<FingerprintSensorPropertiesInternal> mFingerprintSensorProperties;
     private final int mCurrentUserId = 100;
     private final UserInfo mCurrentUserInfo = new UserInfo(mCurrentUserId, "Test user", 0);
 
@@ -280,9 +281,7 @@
         when(mFaceSensorProperties.get(anyInt())).thenReturn(
                 createFaceSensorProperties(/* supportsFaceDetection = */ false));
 
-        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
-        when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
-        when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(List.of(
+        mFingerprintSensorProperties = List.of(
                 new FingerprintSensorPropertiesInternal(1 /* sensorId */,
                         FingerprintSensorProperties.STRENGTH_STRONG,
                         1 /* maxEnrollmentsPerUser */,
@@ -291,7 +290,11 @@
                                 "1.01" /* firmwareVersion */,
                                 "00000001" /* serialNumber */, "" /* softwareVersion */)),
                         FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
-                        false /* resetLockoutRequiresHAT */)));
+                        false /* resetLockoutRequiresHAT */));
+        when(mFingerprintManager.isHardwareDetected()).thenReturn(true);
+        when(mFingerprintManager.hasEnrolledTemplates(anyInt())).thenReturn(true);
+        when(mFingerprintManager.getSensorPropertiesInternal()).thenReturn(
+                mFingerprintSensorProperties);
         when(mUserManager.isUserUnlocked(anyInt())).thenReturn(true);
         when(mUserManager.isPrimaryUser()).thenReturn(true);
         when(mStrongAuthTracker.getStub()).thenReturn(mock(IStrongAuthTracker.Stub.class));
@@ -770,12 +773,43 @@
     }
 
     @Test
+    public void nofaceDetect_whenStrongAuthRequiredAndBypassUdfpsSupportedAndFpRunning() {
+        // GIVEN mocked keyguardUpdateMonitorCallback
+        KeyguardUpdateMonitorCallback keyguardUpdateMonitorCallback =
+                mock(KeyguardUpdateMonitorCallback.class);
+        mKeyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback);
+
+        // GIVEN bypass is enabled, face detection is supported
+        lockscreenBypassIsAllowed();
+        supportsFaceDetection();
+        keyguardIsVisible();
+
+        // GIVEN udfps is supported and strong auth required for weak biometrics (face) only
+        givenUdfpsSupported();
+        strongAuthRequiredForWeakBiometricOnly(); // this allows fingerprint to run but not face
+
+        // WHEN the device wakes up
+        mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
+        mTestableLooper.processAllMessages();
+
+        // THEN face detect and authenticate are NOT triggered
+        verify(mFaceManager, never()).detectFace(any(), any(), anyInt());
+        verify(mFaceManager, never()).authenticate(any(), any(), any(), any(), anyInt(),
+                anyBoolean());
+
+        // THEN biometric help message sent to callback
+        verify(keyguardUpdateMonitorCallback).onBiometricHelp(
+                eq(BIOMETRIC_HELP_FACE_NOT_AVAILABLE), anyString(), eq(BiometricSourceType.FACE));
+    }
+
+    @Test
     public void faceDetect_whenStrongAuthRequiredAndBypass() {
         // GIVEN bypass is enabled, face detection is supported and strong auth is required
         lockscreenBypassIsAllowed();
         supportsFaceDetection();
         strongAuthRequiredEncrypted();
         keyguardIsVisible();
+        // fingerprint is NOT running, UDFPS is NOT supported
 
         // WHEN the device wakes up
         mKeyguardUpdateMonitor.dispatchStartedWakingUp(PowerManager.WAKE_REASON_POWER_BUTTON);
@@ -1733,7 +1767,7 @@
     }
 
     @Test
-    public void testShouldListenForFace_whenOccludingAppRequestsFaceAuth_returnsTrue()
+    public void shouldListenForFace_secureCameraLaunchedButAlternateBouncerIsLaunched_returnsTrue()
             throws RemoteException {
         // Face auth should run when the following is true.
         keyguardNotGoingAway();
@@ -1749,7 +1783,7 @@
 
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
 
-        occludingAppRequestsFaceAuth();
+        alternateBouncerVisible();
         mTestableLooper.processAllMessages();
 
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
@@ -1839,7 +1873,7 @@
     }
 
     @Test
-    public void testShouldListenForFace_whenUdfpsBouncerIsShowing_returnsTrue()
+    public void testShouldListenForFace_whenAlternateBouncerIsShowing_returnsTrue()
             throws RemoteException {
         // Preconditions for face auth to run
         keyguardNotGoingAway();
@@ -1851,13 +1885,13 @@
         mTestableLooper.processAllMessages();
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
 
-        mKeyguardUpdateMonitor.setUdfpsBouncerShowing(true);
+        mKeyguardUpdateMonitor.setAlternateBouncerShowing(true);
 
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
     }
 
     @Test
-    public void testShouldListenForFace_udfpsBouncerIsShowingButDeviceGoingToSleep_returnsFalse()
+    public void testShouldListenForFace_alternateBouncerShowingButDeviceGoingToSleep_returnsFalse()
             throws RemoteException {
         // Preconditions for face auth to run
         keyguardNotGoingAway();
@@ -1867,7 +1901,7 @@
         biometricsEnabledForCurrentUser();
         userNotCurrentlySwitching();
         deviceNotGoingToSleep();
-        mKeyguardUpdateMonitor.setUdfpsBouncerShowing(true);
+        alternateBouncerVisible();
         mTestableLooper.processAllMessages();
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
 
@@ -1877,6 +1911,10 @@
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isFalse();
     }
 
+    private void alternateBouncerVisible() {
+        mKeyguardUpdateMonitor.setAlternateBouncerShowing(true);
+    }
+
     @Test
     public void testShouldListenForFace_whenFaceIsLockedOut_returnsTrue()
             throws RemoteException {
@@ -1887,7 +1925,7 @@
         biometricsNotDisabledThroughDevicePolicyManager();
         biometricsEnabledForCurrentUser();
         userNotCurrentlySwitching();
-        mKeyguardUpdateMonitor.setUdfpsBouncerShowing(true);
+        mKeyguardUpdateMonitor.setAlternateBouncerShowing(true);
         mTestableLooper.processAllMessages();
         assertThat(mKeyguardUpdateMonitor.shouldListenForFace()).isTrue();
 
@@ -2466,6 +2504,11 @@
         when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(false);
     }
 
+    private void strongAuthRequiredForWeakBiometricOnly() {
+        when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(eq(true))).thenReturn(true);
+        when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(eq(false))).thenReturn(false);
+    }
+
     private void strongAuthNotRequired() {
         when(mStrongAuthTracker.getStrongAuthForUser(KeyguardUpdateMonitor.getCurrentUser()))
                 .thenReturn(0);
@@ -2520,6 +2563,12 @@
         mTestableLooper.processAllMessages();
     }
 
+    private void givenUdfpsSupported() {
+        Assert.assertFalse(mFingerprintSensorProperties.isEmpty());
+        when(mAuthController.getUdfpsProps()).thenReturn(mFingerprintSensorProperties);
+        Assert.assertTrue(mKeyguardUpdateMonitor.isUdfpsSupported());
+    }
+
     private void setBroadcastReceiverPendingResult(BroadcastReceiver receiver) {
         BroadcastReceiver.PendingResult pendingResult =
                 new BroadcastReceiver.PendingResult(Activity.RESULT_OK,
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
index 05bd1e4..3d0d036 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/keyguard/LockIconViewControllerBaseTest.java
@@ -159,7 +159,9 @@
                 mAuthRippleController,
                 mResources,
                 new KeyguardTransitionInteractor(mTransitionRepository),
-                new KeyguardInteractor(new FakeKeyguardRepository(), mCommandQueue),
+                new KeyguardInteractor(new FakeKeyguardRepository(),
+                        mCommandQueue,
+                        mFeatureFlags),
                 mFeatureFlags
         );
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
index 32edf8f..babbe45 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/ChooserSelectorTest.kt
@@ -11,6 +11,7 @@
 import com.android.systemui.flags.Flag
 import com.android.systemui.flags.FlagListenable
 import com.android.systemui.flags.Flags
+import com.android.systemui.flags.ReleasedFlag
 import com.android.systemui.flags.UnreleasedFlag
 import com.android.systemui.settings.UserTracker
 import com.android.systemui.util.mockito.any
@@ -102,7 +103,7 @@
     @Test
     fun initialize_enablesUnbundledChooser_whenFlagEnabled() {
         // Arrange
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+        setFlagMock(true)
 
         // Act
         chooserSelector.start()
@@ -118,7 +119,7 @@
     @Test
     fun initialize_disablesUnbundledChooser_whenFlagDisabled() {
         // Arrange
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        setFlagMock(false)
 
         // Act
         chooserSelector.start()
@@ -134,7 +135,7 @@
     @Test
     fun enablesUnbundledChooser_whenFlagBecomesEnabled() {
         // Arrange
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        setFlagMock(false)
         chooserSelector.start()
         verify(mockFeatureFlags).addListener(
                 eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
@@ -147,7 +148,7 @@
         )
 
         // Act
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+        setFlagMock(true)
         flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
 
         // Assert
@@ -161,7 +162,7 @@
     @Test
     fun disablesUnbundledChooser_whenFlagBecomesDisabled() {
         // Arrange
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(true)
+        setFlagMock(true)
         chooserSelector.start()
         verify(mockFeatureFlags).addListener(
                 eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
@@ -174,7 +175,7 @@
         )
 
         // Act
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        setFlagMock(false)
         flagListener.value.onFlagChanged(TestFlagEvent(Flags.CHOOSER_UNBUNDLED.name))
 
         // Assert
@@ -188,7 +189,7 @@
     @Test
     fun doesNothing_whenAnotherFlagChanges() {
         // Arrange
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
+        setFlagMock(false)
         chooserSelector.start()
         verify(mockFeatureFlags).addListener(
                 eq<Flag<*>>(Flags.CHOOSER_UNBUNDLED),
@@ -197,13 +198,17 @@
         clearInvocations(mockPackageManager)
 
         // Act
-        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(false)
         flagListener.value.onFlagChanged(TestFlagEvent("other flag"))
 
         // Assert
         verifyZeroInteractions(mockPackageManager)
     }
 
+    private fun setFlagMock(enabled: Boolean) {
+        whenever(mockFeatureFlags.isEnabled(any<UnreleasedFlag>())).thenReturn(enabled)
+        whenever(mockFeatureFlags.isEnabled(any<ReleasedFlag>())).thenReturn(enabled)
+    }
+
     private class TestFlagEvent(override val flagName: String) : FlagListenable.FlagEvent {
         override fun requestNoRestart() {}
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
index fb54d6d..4415033 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/CustomizationProviderTest.kt
@@ -157,25 +157,28 @@
                 dumpManager = mock(),
                 userHandle = UserHandle.SYSTEM,
             )
+        val featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
+                set(Flags.LOCKSCREEN_CUSTOM_CLOCKS, true)
+                set(Flags.REVAMPED_WALLPAPER_UI, true)
+                set(Flags.WALLPAPER_FULLSCREEN_PREVIEW, true)
+                set(Flags.FACE_AUTH_REFACTOR, true)
+            }
         underTest.interactor =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor =
                     KeyguardInteractor(
                         repository = FakeKeyguardRepository(),
                         commandQueue = commandQueue,
+                        featureFlags = featureFlags,
                     ),
                 registry = mock(),
                 lockPatternUtils = lockPatternUtils,
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
                 activityStarter = activityStarter,
-                featureFlags =
-                    FakeFeatureFlags().apply {
-                        set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, true)
-                        set(Flags.LOCKSCREEN_CUSTOM_CLOCKS, true)
-                        set(Flags.REVAMPED_WALLPAPER_UI, true)
-                        set(Flags.WALLPAPER_FULLSCREEN_PREVIEW, true)
-                    },
+                featureFlags = featureFlags,
                 repository = { quickAffordanceRepository },
                 launchAnimator = launchAnimator,
             )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
index 2290676..c3b0e5226 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardIndicationRotateTextViewControllerTest.java
@@ -38,6 +38,7 @@
 
 import androidx.test.filters.SmallTest;
 
+import com.android.keyguard.logging.KeyguardLogger;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.plugins.statusbar.StatusBarStateController;
 import com.android.systemui.statusbar.phone.KeyguardIndicationTextView;
@@ -66,6 +67,8 @@
     private KeyguardIndicationTextView mView;
     @Mock
     private StatusBarStateController mStatusBarStateController;
+    @Mock
+    private KeyguardLogger mLogger;
     @Captor
     private ArgumentCaptor<StatusBarStateController.StateListener> mStatusBarStateListenerCaptor;
 
@@ -77,7 +80,7 @@
         MockitoAnnotations.initMocks(this);
         when(mView.getTextColors()).thenReturn(ColorStateList.valueOf(Color.WHITE));
         mController = new KeyguardIndicationRotateTextViewController(mView, mExecutor,
-                mStatusBarStateController);
+                mStatusBarStateController, mLogger);
         mController.onViewAttached();
 
         verify(mStatusBarStateController).addCallback(mStatusBarStateListenerCaptor.capture());
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
index 58cdec4..db18ba6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt
@@ -20,6 +20,7 @@
 import android.app.StatusBarManager
 import android.content.Context
 import android.content.pm.PackageManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.camera.CameraGestureHelper
@@ -31,14 +32,13 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class CameraQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var cameraGestureHelper: CameraGestureHelper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
index 15b85de..64839e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
@@ -22,6 +22,7 @@
 import android.provider.Settings.Global.ZEN_MODE_OFF
 import android.provider.Settings.Secure.ZEN_DURATION_FOREVER
 import android.provider.Settings.Secure.ZEN_DURATION_PROMPT
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.settingslib.notification.EnableZenModeDialog
 import com.android.systemui.R
@@ -51,7 +52,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
@@ -60,7 +60,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class DoNotDisturbQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var zenModeController: ZenModeController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
index 9fa7db1..31391ee 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.data.quickaffordance
 
 import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.common.shared.model.Icon
@@ -35,13 +36,12 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() {
 
     @Mock private lateinit var context: Context
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
index 659c1e5..2c1c04c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/HomeControlsKeyguardQuickAffordanceConfigTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.data.quickaffordance
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -34,13 +35,12 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.`when` as whenever
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class HomeControlsKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var component: ControlsComponent
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
index 3b0169d..3bae7f7 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLegacySettingSyncerTest.kt
@@ -20,6 +20,7 @@
 import android.content.Context
 import android.content.res.Resources
 import android.provider.Settings
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -40,7 +41,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
@@ -48,7 +48,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardQuickAffordanceLegacySettingSyncerTest : SysuiTestCase() {
 
     @Mock private lateinit var sharedPrefs: FakeSharedPreferences
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
index 3d65713..1259b47 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceLocalUserSelectionManagerTest.kt
@@ -20,6 +20,7 @@
 import android.content.Intent
 import android.content.SharedPreferences
 import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -40,7 +41,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
@@ -51,7 +51,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardQuickAffordanceLocalUserSelectionManagerTest : SysuiTestCase() {
 
     @Mock private lateinit var userFileManager: UserFileManager
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt
index b21cec9..c08ef42 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceRemoteUserSelectionManagerTest.kt
@@ -19,6 +19,7 @@
 
 import android.content.pm.UserInfo
 import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.settings.FakeUserTracker
@@ -37,13 +38,12 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardQuickAffordanceRemoteUserSelectionManagerTest : SysuiTestCase() {
 
     @Mock private lateinit var userHandle: UserHandle
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
index 34f3ed8..facc747 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/MuteQuickAffordanceCoreStartableTest.kt
@@ -21,6 +21,7 @@
 import android.media.AudioManager
 import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Observer
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.flags.FeatureFlags
@@ -47,7 +48,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.Mockito.verifyZeroInteractions
@@ -55,8 +55,8 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
-class MuteQuickAffordanceCoreStartableTest : SysuiTestCase()  {
+@RunWith(AndroidJUnit4::class)
+class MuteQuickAffordanceCoreStartableTest : SysuiTestCase() {
 
     @Mock
     private lateinit var featureFlags: FeatureFlags
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 9d2ddff..1adf808 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
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.data.quickaffordance
 
 import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig.OnTriggeredResult
@@ -33,13 +34,12 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var controller: QRCodeScannerController
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 8f56b95..752963f 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
@@ -20,6 +20,7 @@
 import android.graphics.drawable.Drawable
 import android.service.quickaccesswallet.GetWalletCardsResponse
 import android.service.quickaccesswallet.QuickAccessWalletClient
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -41,14 +42,13 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class QuickAccessWalletKeyguardQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var walletController: QuickAccessWalletController
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt
index 805dcec..5bd86bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/quickaffordance/VideoCameraQuickAffordanceConfigTest.kt
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.data.quickaffordance
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.ActivityIntentHelper
 import com.android.systemui.SysuiTestCase
@@ -32,7 +33,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.Mock
@@ -40,7 +40,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class VideoCameraQuickAffordanceConfigTest : SysuiTestCase() {
 
     @Mock private lateinit var activityIntentHelper: ActivityIntentHelper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
index 444a2a7..ff22f1e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBouncerRepositoryTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.data.repository
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.ViewMediatorCallback
 import com.android.systemui.SysuiTestCase
@@ -26,13 +27,12 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardBouncerRepositoryTest : SysuiTestCase() {
 
     @Mock private lateinit var systemClock: SystemClock
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
index 6099f01..86e8c9a 100644
--- 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
@@ -19,9 +19,11 @@
 
 import android.content.pm.UserInfo
 import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
 import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
 import com.android.systemui.keyguard.data.quickaffordance.KeyguardQuickAffordanceConfig
@@ -39,23 +41,19 @@
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.settings.FakeSettings
 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.yield
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardQuickAffordanceRepositoryTest : SysuiTestCase() {
 
     private lateinit var underTest: KeyguardQuickAffordanceRepository
@@ -65,12 +63,14 @@
     private lateinit var userTracker: FakeUserTracker
     private lateinit var client1: FakeCustomizationProviderClient
     private lateinit var client2: FakeCustomizationProviderClient
+    private lateinit var testScope: TestScope
 
     @Before
     fun setUp() {
         config1 = FakeKeyguardQuickAffordanceConfig(FakeCustomizationProviderClient.AFFORDANCE_1)
         config2 = FakeKeyguardQuickAffordanceConfig(FakeCustomizationProviderClient.AFFORDANCE_2)
-        val scope = CoroutineScope(IMMEDIATE)
+        val testDispatcher = StandardTestDispatcher()
+        testScope = TestScope(testDispatcher)
         userTracker = FakeUserTracker()
         val localUserSelectionManager =
             KeyguardQuickAffordanceLocalUserSelectionManager(
@@ -93,7 +93,7 @@
         client2 = FakeCustomizationProviderClient()
         val remoteUserSelectionManager =
             KeyguardQuickAffordanceRemoteUserSelectionManager(
-                scope = scope,
+                scope = testScope.backgroundScope,
                 userTracker = userTracker,
                 clientFactory =
                     FakeKeyguardQuickAffordanceProviderClientFactory(
@@ -116,14 +116,14 @@
         underTest =
             KeyguardQuickAffordanceRepository(
                 appContext = context,
-                scope = scope,
+                scope = testScope.backgroundScope,
                 localUserSelectionManager = localUserSelectionManager,
                 remoteUserSelectionManager = remoteUserSelectionManager,
                 userTracker = userTracker,
                 legacySettingSyncer =
                     KeyguardQuickAffordanceLegacySettingSyncer(
-                        scope = scope,
-                        backgroundDispatcher = IMMEDIATE,
+                        scope = testScope.backgroundScope,
+                        backgroundDispatcher = testDispatcher,
                         secureSettings = FakeSettings(),
                         selectionsManager = localUserSelectionManager,
                     ),
@@ -135,15 +135,14 @@
 
     @Test
     fun setSelections() =
-        runBlocking(IMMEDIATE) {
-            var configsBySlotId: Map<String, List<KeyguardQuickAffordanceConfig>>? = null
-            val job = underTest.selections.onEach { configsBySlotId = it }.launchIn(this)
+        testScope.runTest {
+            val configsBySlotId = collectLastValue(underTest.selections)
             val slotId1 = "slot1"
             val slotId2 = "slot2"
 
             underTest.setSelections(slotId1, listOf(config1.key))
             assertSelections(
-                configsBySlotId,
+                configsBySlotId(),
                 mapOf(
                     slotId1 to listOf(config1),
                 ),
@@ -151,7 +150,7 @@
 
             underTest.setSelections(slotId2, listOf(config2.key))
             assertSelections(
-                configsBySlotId,
+                configsBySlotId(),
                 mapOf(
                     slotId1 to listOf(config1),
                     slotId2 to listOf(config2),
@@ -161,19 +160,17 @@
             underTest.setSelections(slotId1, emptyList())
             underTest.setSelections(slotId2, listOf(config1.key))
             assertSelections(
-                configsBySlotId,
+                configsBySlotId(),
                 mapOf(
                     slotId1 to emptyList(),
                     slotId2 to listOf(config1),
                 ),
             )
-
-            job.cancel()
         }
 
     @Test
     fun getAffordancePickerRepresentations() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             assertThat(underTest.getAffordancePickerRepresentations())
                 .isEqualTo(
                     listOf(
@@ -226,7 +223,7 @@
 
     @Test
     fun `selections for secondary user`() =
-        runBlocking(IMMEDIATE) {
+        testScope.runTest {
             userTracker.set(
                 userInfos =
                     listOf(
@@ -252,12 +249,10 @@
                 slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START,
                 affordanceId = FakeCustomizationProviderClient.AFFORDANCE_2,
             )
-            val observed = mutableListOf<Map<String, List<KeyguardQuickAffordanceConfig>>>()
-            val job = underTest.selections.onEach { observed.add(it) }.launchIn(this)
-            yield()
+            val observed = collectLastValue(underTest.selections)
 
             assertSelections(
-                observed = observed.last(),
+                observed = observed(),
                 expected =
                     mapOf(
                         KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START to
@@ -266,8 +261,6 @@
                             ),
                     )
             )
-
-            job.cancel()
         }
 
     private fun assertSelections(
@@ -283,7 +276,6 @@
     }
 
     companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
         private const val SECONDARY_USER_1 = UserHandle.MIN_SECONDARY_USER_ID + 1
         private const val SECONDARY_USER_2 = UserHandle.MIN_SECONDARY_USER_ID + 2
     }
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 f997d18..8bb6a85 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
@@ -18,6 +18,7 @@
 
 import android.graphics.Point
 import android.hardware.biometrics.BiometricSourceType
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.KeyguardUpdateMonitorCallback
@@ -54,13 +55,12 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardRepositoryImplTest : SysuiTestCase() {
 
     @Mock private lateinit var statusBarStateController: StatusBarStateController
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 32cec09..ae227b4 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
@@ -20,6 +20,7 @@
 import android.util.Log
 import android.util.Log.TerribleFailure
 import android.util.Log.TerribleFailureHandler
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Interpolators
@@ -43,10 +44,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardTransitionRepositoryTest : SysuiTestCase() {
 
     private lateinit var underTest: KeyguardTransitionRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
index 4b06905..a181137 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt
@@ -18,6 +18,7 @@
 
 import android.app.trust.TrustManager
 import android.content.pm.UserInfo
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.logging.TrustRepositoryLogger
 import com.android.systemui.SysuiTestCase
@@ -33,7 +34,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentCaptor
 import org.mockito.Captor
 import org.mockito.Mock
@@ -43,7 +43,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class TrustRepositoryTest : SysuiTestCase() {
     @Mock private lateinit var trustManager: TrustManager
     @Captor private lateinit var listenerCaptor: ArgumentCaptor<TrustManager.TrustListener>
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
index 8caf60f..7ded354 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/AlternateBouncerInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.keyguard.ViewMediatorCallback
@@ -36,14 +37,13 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.mock
 import org.mockito.MockitoAnnotations
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class AlternateBouncerInteractorTest : SysuiTestCase() {
     private lateinit var underTest: AlternateBouncerInteractor
     private lateinit var bouncerRepository: KeyguardBouncerRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index 68d13d3..7d4861b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -18,30 +18,35 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.app.StatusBarManager
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel
+import com.android.systemui.settings.DisplayTracker
 import com.android.systemui.statusbar.CommandQueue
-import com.android.systemui.statusbar.CommandQueue.Callbacks
-import com.android.systemui.util.mockito.argumentCaptor
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.flow.onCompletion
+import kotlinx.coroutines.test.TestScope
 import kotlinx.coroutines.test.runCurrent
 import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-import org.mockito.Mock
+import org.mockito.Mockito.mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardInteractorTest : SysuiTestCase() {
-    @Mock private lateinit var commandQueue: CommandQueue
+    private lateinit var commandQueue: FakeCommandQueue
+    private lateinit var featureFlags: FakeFeatureFlags
+    private lateinit var testScope: TestScope
 
     private lateinit var underTest: KeyguardInteractor
     private lateinit var repository: FakeKeyguardRepository
@@ -49,38 +54,134 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-
+        featureFlags = FakeFeatureFlags().apply { set(FACE_AUTH_REFACTOR, true) }
+        commandQueue = FakeCommandQueue(mock(Context::class.java), mock(DisplayTracker::class.java))
+        testScope = TestScope()
         repository = FakeKeyguardRepository()
-        underTest = KeyguardInteractor(repository, commandQueue)
+        underTest = KeyguardInteractor(repository, commandQueue, featureFlags)
     }
 
     @Test
-    fun onCameraLaunchDetected() = runTest {
-        val flow = underTest.onCameraLaunchDetected
-        var cameraLaunchSource = collectLastValue(flow)
-        runCurrent()
+    fun onCameraLaunchDetected() =
+        testScope.runTest {
+            val flow = underTest.onCameraLaunchDetected
+            var cameraLaunchSource = collectLastValue(flow)
+            runCurrent()
 
-        val captor = argumentCaptor<CommandQueue.Callbacks>()
-        verify(commandQueue).addCallback(captor.capture())
+            commandQueue.doForEachCallback {
+                it.onCameraLaunchGestureDetected(StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE)
+            }
+            assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.WIGGLE)
 
-        captor.value.onCameraLaunchGestureDetected(StatusBarManager.CAMERA_LAUNCH_SOURCE_WIGGLE)
-        assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.WIGGLE)
+            commandQueue.doForEachCallback {
+                it.onCameraLaunchGestureDetected(
+                    StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP
+                )
+            }
+            assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.POWER_DOUBLE_TAP)
 
-        captor.value.onCameraLaunchGestureDetected(
-            StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP
-        )
-        assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.POWER_DOUBLE_TAP)
+            commandQueue.doForEachCallback {
+                it.onCameraLaunchGestureDetected(StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER)
+            }
+            assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.LIFT_TRIGGER)
 
-        captor.value.onCameraLaunchGestureDetected(
-            StatusBarManager.CAMERA_LAUNCH_SOURCE_LIFT_TRIGGER
-        )
-        assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.LIFT_TRIGGER)
+            commandQueue.doForEachCallback {
+                it.onCameraLaunchGestureDetected(
+                    StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE
+                )
+            }
+            assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.QUICK_AFFORDANCE)
 
-        captor.value.onCameraLaunchGestureDetected(
-            StatusBarManager.CAMERA_LAUNCH_SOURCE_QUICK_AFFORDANCE
-        )
-        assertThat(cameraLaunchSource()).isEqualTo(CameraLaunchSourceModel.QUICK_AFFORDANCE)
+            flow.onCompletion { assertThat(commandQueue.callbackCount()).isEqualTo(0) }
+        }
 
-        flow.onCompletion { verify(commandQueue).removeCallback(captor.value) }
+    @Test
+    fun testKeyguardGuardVisibilityStopsSecureCamera() =
+        testScope.runTest {
+            val secureCameraActive = collectLastValue(underTest.isSecureCameraActive)
+            runCurrent()
+
+            commandQueue.doForEachCallback {
+                it.onCameraLaunchGestureDetected(
+                    StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP
+                )
+            }
+
+            assertThat(secureCameraActive()).isTrue()
+
+            // Keyguard is showing but occluded
+            repository.setKeyguardShowing(true)
+            repository.setKeyguardOccluded(true)
+            assertThat(secureCameraActive()).isTrue()
+
+            // Keyguard is showing and not occluded
+            repository.setKeyguardOccluded(false)
+            assertThat(secureCameraActive()).isFalse()
+        }
+
+    @Test
+    fun testBouncerShowingResetsSecureCameraState() =
+        testScope.runTest {
+            val secureCameraActive = collectLastValue(underTest.isSecureCameraActive)
+            runCurrent()
+
+            commandQueue.doForEachCallback {
+                it.onCameraLaunchGestureDetected(
+                    StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP
+                )
+            }
+            assertThat(secureCameraActive()).isTrue()
+
+            // Keyguard is showing and not occluded
+            repository.setKeyguardShowing(true)
+            repository.setKeyguardOccluded(true)
+            assertThat(secureCameraActive()).isTrue()
+
+            repository.setBouncerShowing(true)
+            assertThat(secureCameraActive()).isFalse()
+        }
+
+    @Test
+    fun keyguardVisibilityIsDefinedAsKeyguardShowingButNotOccluded() = runTest {
+        var isVisible = collectLastValue(underTest.isKeyguardVisible)
+        repository.setKeyguardShowing(true)
+        repository.setKeyguardOccluded(false)
+
+        assertThat(isVisible()).isTrue()
+
+        repository.setKeyguardOccluded(true)
+        assertThat(isVisible()).isFalse()
+
+        repository.setKeyguardShowing(false)
+        repository.setKeyguardOccluded(true)
+        assertThat(isVisible()).isFalse()
     }
+
+    @Test
+    fun secureCameraIsNotActiveWhenNoCameraLaunchEventHasBeenFiredYet() =
+        testScope.runTest {
+            val secureCameraActive = collectLastValue(underTest.isSecureCameraActive)
+            runCurrent()
+
+            assertThat(secureCameraActive()).isFalse()
+        }
+}
+
+class FakeCommandQueue(val context: Context, val displayTracker: DisplayTracker) :
+    CommandQueue(context, displayTracker) {
+    private val callbacks = mutableListOf<Callbacks>()
+
+    override fun addCallback(callback: Callbacks) {
+        callbacks.add(callback)
+    }
+
+    override fun removeCallback(callback: Callbacks) {
+        callbacks.remove(callback)
+    }
+
+    fun doForEachCallback(func: (callback: Callbacks) -> Unit) {
+        callbacks.forEach { func(it) }
+    }
+
+    fun callbackCount(): Int = callbacks.size
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
index 9d60b16..51988ef 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardLongPressInteractorTest.kt
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.UiEventLogger
 import com.android.systemui.SysuiTestCase
@@ -39,7 +40,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyBoolean
 import org.mockito.Mock
 import org.mockito.Mockito.never
@@ -48,7 +48,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardLongPressInteractorTest : SysuiTestCase() {
 
     @Mock private lateinit var activityStarter: ActivityStarter
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 43287b0..240af7b 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
@@ -286,12 +286,18 @@
                 dumpManager = mock(),
                 userHandle = UserHandle.SYSTEM,
             )
+        val featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+                set(Flags.FACE_AUTH_REFACTOR, true)
+            }
         underTest =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor =
                     KeyguardInteractor(
                         repository = FakeKeyguardRepository(),
-                        commandQueue = commandQueue
+                        commandQueue = commandQueue,
+                        featureFlags = featureFlags,
                     ),
                 registry =
                     FakeKeyguardQuickAffordanceRegistry(
@@ -311,10 +317,7 @@
                 keyguardStateController = keyguardStateController,
                 userTracker = userTracker,
                 activityStarter = activityStarter,
-                featureFlags =
-                    FakeFeatureFlags().apply {
-                        set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
-                    },
+                featureFlags = featureFlags,
                 repository = { quickAffordanceRepository },
                 launchAnimator = launchAnimator,
             )
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 b75a15d..ec70857 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
@@ -18,6 +18,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.os.UserHandle
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.internal.widget.LockPatternUtils
 import com.android.systemui.R
@@ -60,7 +61,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.ArgumentMatchers.anyInt
 import org.mockito.ArgumentMatchers.anyString
 import org.mockito.Mock
@@ -68,7 +68,7 @@
 
 @OptIn(ExperimentalCoroutinesApi::class)
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
 
     @Mock private lateinit var lockPatternUtils: LockPatternUtils
@@ -150,12 +150,17 @@
         featureFlags =
             FakeFeatureFlags().apply {
                 set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+                set(Flags.FACE_AUTH_REFACTOR, true)
             }
 
         underTest =
             KeyguardQuickAffordanceInteractor(
                 keyguardInteractor =
-                    KeyguardInteractor(repository = repository, commandQueue = commandQueue),
+                    KeyguardInteractor(
+                        repository = repository,
+                        commandQueue = commandQueue,
+                        featureFlags = featureFlags
+                    ),
                 registry =
                     FakeKeyguardQuickAffordanceRegistry(
                         mapOf(
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 6333b24..3d13d80 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
@@ -17,6 +17,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -29,17 +30,17 @@
 import com.android.systemui.keyguard.shared.model.TransitionState.STARTED
 import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
 class KeyguardTransitionInteractorTest : SysuiTestCase() {
 
     private lateinit var underTest: KeyguardTransitionInteractor
@@ -53,7 +54,7 @@
 
     @Test
     fun `transition collectors receives only appropriate events`() =
-        runBlocking(IMMEDIATE) {
+        runTest(UnconfinedTestDispatcher()) {
             var lockscreenToAodSteps = mutableListOf<TransitionStep>()
             val job1 =
                 underTest.lockscreenToAodTransition
@@ -87,10 +88,9 @@
 
     @Test
     fun dozeAmountTransitionTest() =
-        runBlocking(IMMEDIATE) {
+        runTest(UnconfinedTestDispatcher()) {
             var dozeAmountSteps = mutableListOf<TransitionStep>()
-            val job =
-                underTest.dozeAmountTransition.onEach { dozeAmountSteps.add(it) }.launchIn(this)
+            val job = underTest.dozeAmountTransition.onEach { dozeAmountSteps.add(it) }.launchIn(this)
 
             val steps = mutableListOf<TransitionStep>()
 
@@ -119,10 +119,9 @@
 
     @Test
     fun keyguardStateTests() =
-        runBlocking(IMMEDIATE) {
+        runTest(UnconfinedTestDispatcher()) {
             var finishedSteps = mutableListOf<KeyguardState>()
-            val job =
-                underTest.finishedKeyguardState.onEach { finishedSteps.add(it) }.launchIn(this)
+            val job = underTest.finishedKeyguardState.onEach { finishedSteps.add(it) }.launchIn(this)
 
             val steps = mutableListOf<TransitionStep>()
 
@@ -143,12 +142,10 @@
 
     @Test
     fun finishedKeyguardTransitionStepTests() =
-        runBlocking(IMMEDIATE) {
+        runTest(UnconfinedTestDispatcher()) {
             var finishedSteps = mutableListOf<TransitionStep>()
             val job =
-                underTest.finishedKeyguardTransitionStep
-                    .onEach { finishedSteps.add(it) }
-                    .launchIn(this)
+                underTest.finishedKeyguardTransitionStep.onEach { finishedSteps.add(it) }.launchIn(this)
 
             val steps = mutableListOf<TransitionStep>()
 
@@ -169,12 +166,10 @@
 
     @Test
     fun startedKeyguardTransitionStepTests() =
-        runBlocking(IMMEDIATE) {
+        runTest(UnconfinedTestDispatcher()) {
             var startedSteps = mutableListOf<TransitionStep>()
             val job =
-                underTest.startedKeyguardTransitionStep
-                    .onEach { startedSteps.add(it) }
-                    .launchIn(this)
+                underTest.startedKeyguardTransitionStep.onEach { startedSteps.add(it) }.launchIn(this)
 
             val steps = mutableListOf<TransitionStep>()
 
@@ -192,8 +187,4 @@
 
             job.cancel()
         }
-
-    companion object {
-        private val IMMEDIATE = Dispatchers.Main.immediate
-    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 702f3763..46e4679 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -21,6 +21,8 @@
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.animation.Interpolators
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository
 import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositoryImpl
@@ -92,10 +94,12 @@
         transitionRepository = KeyguardTransitionRepositoryImpl()
         runner = KeyguardTransitionRunner(transitionRepository)
 
+        val featureFlags = FakeFeatureFlags().apply { set(Flags.FACE_AUTH_REFACTOR, true) }
         fromLockscreenTransitionInteractor =
             FromLockscreenTransitionInteractor(
                 scope = testScope,
-                keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+                keyguardInteractor =
+                    KeyguardInteractor(keyguardRepository, commandQueue, featureFlags),
                 shadeRepository = shadeRepository,
                 keyguardTransitionRepository = mockTransitionRepository,
                 keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
@@ -105,7 +109,8 @@
         fromDreamingTransitionInteractor =
             FromDreamingTransitionInteractor(
                 scope = testScope,
-                keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+                keyguardInteractor =
+                    KeyguardInteractor(keyguardRepository, commandQueue, featureFlags),
                 keyguardTransitionRepository = mockTransitionRepository,
                 keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
             )
@@ -114,7 +119,8 @@
         fromAodTransitionInteractor =
             FromAodTransitionInteractor(
                 scope = testScope,
-                keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+                keyguardInteractor =
+                    KeyguardInteractor(keyguardRepository, commandQueue, featureFlags),
                 keyguardTransitionRepository = mockTransitionRepository,
                 keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
             )
@@ -123,7 +129,8 @@
         fromGoneTransitionInteractor =
             FromGoneTransitionInteractor(
                 scope = testScope,
-                keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+                keyguardInteractor =
+                    KeyguardInteractor(keyguardRepository, commandQueue, featureFlags),
                 keyguardTransitionRepository = mockTransitionRepository,
                 keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
             )
@@ -132,7 +139,8 @@
         fromDozingTransitionInteractor =
             FromDozingTransitionInteractor(
                 scope = testScope,
-                keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+                keyguardInteractor =
+                    KeyguardInteractor(keyguardRepository, commandQueue, featureFlags),
                 keyguardTransitionRepository = mockTransitionRepository,
                 keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
             )
@@ -141,7 +149,8 @@
         fromOccludedTransitionInteractor =
             FromOccludedTransitionInteractor(
                 scope = testScope,
-                keyguardInteractor = KeyguardInteractor(keyguardRepository, commandQueue),
+                keyguardInteractor =
+                    KeyguardInteractor(keyguardRepository, commandQueue, featureFlags),
                 keyguardTransitionRepository = mockTransitionRepository,
                 keyguardTransitionInteractor = KeyguardTransitionInteractor(transitionRepository),
             )
@@ -149,7 +158,7 @@
     }
 
     @Test
-    fun `DREAMING to LOCKSCREEN`() =
+    fun `DREAMING to LOCKSCREEN - dreaming state changes first`() =
         testScope.runTest {
             // GIVEN a device is dreaming and occluded
             keyguardRepository.setDreamingWithOverlay(true)
@@ -179,9 +188,59 @@
             )
             // AND dreaming has stopped
             keyguardRepository.setDreamingWithOverlay(false)
+            advanceUntilIdle()
+            // AND then occluded has stopped
+            keyguardRepository.setKeyguardOccluded(false)
+            advanceUntilIdle()
+
+            val info =
+                withArgCaptor<TransitionInfo> {
+                    verify(mockTransitionRepository).startTransition(capture())
+                }
+            // THEN a transition to BOUNCER should occur
+            assertThat(info.ownerName).isEqualTo("FromDreamingTransitionInteractor")
+            assertThat(info.from).isEqualTo(KeyguardState.DREAMING)
+            assertThat(info.to).isEqualTo(KeyguardState.LOCKSCREEN)
+            assertThat(info.animator).isNotNull()
+
+            coroutineContext.cancelChildren()
+        }
+
+    @Test
+    fun `DREAMING to LOCKSCREEN - occluded state changes first`() =
+        testScope.runTest {
+            // GIVEN a device is dreaming and occluded
+            keyguardRepository.setDreamingWithOverlay(true)
+            keyguardRepository.setKeyguardOccluded(true)
+            runCurrent()
+
+            // GIVEN a prior transition has run to DREAMING
+            runner.startTransition(
+                testScope,
+                TransitionInfo(
+                    ownerName = "",
+                    from = KeyguardState.LOCKSCREEN,
+                    to = KeyguardState.DREAMING,
+                    animator =
+                        ValueAnimator().apply {
+                            duration = 10
+                            interpolator = Interpolators.LINEAR
+                        },
+                )
+            )
+            runCurrent()
+            reset(mockTransitionRepository)
+
+            // WHEN doze is complete
+            keyguardRepository.setDozeTransitionModel(
+                DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+            )
             // AND occluded has stopped
             keyguardRepository.setKeyguardOccluded(false)
             advanceUntilIdle()
+            // AND then dreaming has stopped
+            keyguardRepository.setDreamingWithOverlay(false)
+            advanceUntilIdle()
 
             val info =
                 withArgCaptor<TransitionInfo> {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
index 3166214..6236616 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/LightRevealScrimInteractorTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.domain.interactor
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -33,11 +34,10 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class LightRevealScrimInteractorTest : SysuiTestCase() {
     private val fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository()
     private val fakeLightRevealScrimRepository = FakeLightRevealScrimRepository()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
index fbfeca9..f86ac79 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt
@@ -17,18 +17,18 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito.verify
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() {
     private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor()
     @Mock
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
index ea7bc91..75b74b0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/PrimaryBouncerInteractorWithCoroutinesTest.kt
@@ -17,6 +17,7 @@
 package com.android.systemui.keyguard.domain.interactor
 
 import android.os.Looper
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardSecurityModel
 import com.android.keyguard.KeyguardUpdateMonitor
@@ -35,12 +36,11 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class PrimaryBouncerInteractorWithCoroutinesTest : SysuiTestCase() {
     private lateinit var repository: FakeKeyguardBouncerRepository
     @Mock private lateinit var bouncerView: BouncerView
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
index 06e397d..706154e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToLockscreenTransitionViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -33,10 +34,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class DreamingToLockscreenTransitionViewModelTest : SysuiTestCase() {
     private lateinit var underTest: DreamingToLockscreenTransitionViewModel
     private lateinit var repository: FakeKeyguardTransitionRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
index 14c3b50..b15ce10 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/GoneToDreamingTransitionViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -32,10 +33,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class GoneToDreamingTransitionViewModelTest : SysuiTestCase() {
     private lateinit var underTest: GoneToDreamingTransitionViewModel
     private lateinit var repository: FakeKeyguardTransitionRepository
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 4b04b7b..03a347e 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
@@ -125,9 +125,18 @@
                 ),
             )
         repository = FakeKeyguardRepository()
+        val featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
+                set(Flags.FACE_AUTH_REFACTOR, true)
+            }
 
         val keyguardInteractor =
-            KeyguardInteractor(repository = repository, commandQueue = commandQueue)
+            KeyguardInteractor(
+                repository = repository,
+                commandQueue = commandQueue,
+                featureFlags = featureFlags,
+            )
         whenever(userTracker.userHandle).thenReturn(mock())
         whenever(lockPatternUtils.getStrongAuthForUser(anyInt()))
             .thenReturn(LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED)
@@ -191,10 +200,7 @@
                         keyguardStateController = keyguardStateController,
                         userTracker = userTracker,
                         activityStarter = activityStarter,
-                        featureFlags =
-                            FakeFeatureFlags().apply {
-                                set(Flags.CUSTOMIZABLE_LOCK_SCREEN_QUICK_AFFORDANCES, false)
-                            },
+                        featureFlags = featureFlags,
                         repository = { quickAffordanceRepository },
                         launchAnimator = launchAnimator,
                     ),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
index 3727134..586af62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBouncerViewModelTest.kt
@@ -16,28 +16,29 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.BouncerView
 import com.android.systemui.keyguard.domain.interactor.PrimaryBouncerInteractor
 import com.android.systemui.keyguard.shared.model.BouncerShowMessageModel
 import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 import org.mockito.Mock
 import org.mockito.Mockito
 import org.mockito.MockitoAnnotations
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
+@kotlinx.coroutines.ExperimentalCoroutinesApi
 class KeyguardBouncerViewModelTest : SysuiTestCase() {
     lateinit var underTest: KeyguardBouncerViewModel
     @Mock lateinit var bouncerView: BouncerView
@@ -51,7 +52,7 @@
 
     @Test
     fun setMessage() =
-        runBlocking(Dispatchers.Main.immediate) {
+        runTest {
             val flow = MutableStateFlow<BouncerShowMessageModel?>(null)
             var message: BouncerShowMessageModel? = null
             Mockito.`when`(bouncerInteractor.showMessage)
@@ -62,6 +63,8 @@
             flow.value = BouncerShowMessageModel(message = "abc", colorStateList = null)
 
             val job = underTest.bouncerShowMessage.onEach { message = it }.launchIn(this)
+            // Run the tasks that are pending at this point of virtual time.
+            runCurrent()
             assertThat(message?.message).isEqualTo("abc")
             job.cancel()
         }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
index ed31dc3..d94c108 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToDreamingTransitionViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -32,10 +33,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class LockscreenToDreamingTransitionViewModelTest : SysuiTestCase() {
     private lateinit var underTest: LockscreenToDreamingTransitionViewModel
     private lateinit var repository: FakeKeyguardTransitionRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
index 458b315..12ec24d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToOccludedTransitionViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -32,10 +33,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class LockscreenToOccludedTransitionViewModelTest : SysuiTestCase() {
     private lateinit var underTest: LockscreenToOccludedTransitionViewModel
     private lateinit var repository: FakeKeyguardTransitionRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
index a36214e..0c4e845 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/OccludedToLockscreenTransitionViewModelTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.keyguard.ui.viewmodel
 
+import androidx.test.ext.junit.runners.AndroidJUnit4
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -32,10 +33,9 @@
 import org.junit.Before
 import org.junit.Test
 import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
 
 @SmallTest
-@RunWith(JUnit4::class)
+@RunWith(AndroidJUnit4::class)
 class OccludedToLockscreenTransitionViewModelTest : SysuiTestCase() {
     private lateinit var underTest: OccludedToLockscreenTransitionViewModel
     private lateinit var repository: FakeKeyguardTransitionRepository
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt
index 3b5e6b9..d1744c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/LogDiffsForTableTest.kt
@@ -276,6 +276,52 @@
         }
 
     @Test
+    fun intNullable_logsNull() =
+        testScope.runTest {
+            systemClock.setCurrentTimeMillis(100L)
+            val flow = flow {
+                for (int in listOf(null, 6, null, 8)) {
+                    systemClock.advanceTime(100L)
+                    emit(int)
+                }
+            }
+
+            val flowWithLogging =
+                flow.logDiffsForTable(
+                    tableLogBuffer,
+                    COLUMN_PREFIX,
+                    COLUMN_NAME,
+                    initialValue = 1234,
+                )
+
+            val job = launch { flowWithLogging.collect() }
+
+            val logs = dumpLog()
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(100L) + SEPARATOR + FULL_NAME + SEPARATOR + "1234"
+                )
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(200L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
+                )
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(300L) + SEPARATOR + FULL_NAME + SEPARATOR + "6"
+                )
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(400L) + SEPARATOR + FULL_NAME + SEPARATOR + "null"
+                )
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(500L) + SEPARATOR + FULL_NAME + SEPARATOR + "8"
+                )
+
+            job.cancel()
+        }
+
+    @Test
     fun int_logsUpdates() =
         testScope.runTest {
             systemClock.setCurrentTimeMillis(100L)
@@ -1030,6 +1076,246 @@
             job.cancel()
         }
 
+    // ---- Flow<List<T>> tests ----
+
+    @Test
+    fun list_doesNotLogWhenNotCollected() {
+        val flow = flowOf(listOf(5), listOf(6), listOf(7))
+
+        flow.logDiffsForTable(
+            tableLogBuffer,
+            COLUMN_PREFIX,
+            COLUMN_NAME,
+            initialValue = listOf(1234),
+        )
+
+        val logs = dumpLog()
+        assertThat(logs).doesNotContain(COLUMN_PREFIX)
+        assertThat(logs).doesNotContain(COLUMN_NAME)
+        assertThat(logs).doesNotContain("1234")
+    }
+
+    @Test
+    fun list_logsInitialWhenCollected() =
+        testScope.runTest {
+            val flow = flowOf(listOf(5), listOf(6), listOf(7))
+
+            val flowWithLogging =
+                flow.logDiffsForTable(
+                    tableLogBuffer,
+                    COLUMN_PREFIX,
+                    COLUMN_NAME,
+                    initialValue = listOf(1234),
+                )
+
+            systemClock.setCurrentTimeMillis(3000L)
+            val job = launch { flowWithLogging.collect() }
+
+            val logs = dumpLog()
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(3000L) +
+                        SEPARATOR +
+                        FULL_NAME +
+                        SEPARATOR +
+                        listOf(1234).toString()
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun list_logsUpdates() =
+        testScope.runTest {
+            systemClock.setCurrentTimeMillis(100L)
+
+            val listItems =
+                listOf(listOf("val1", "val2"), listOf("val3"), listOf("val4", "val5", "val6"))
+            val flow = flow {
+                for (list in listItems) {
+                    systemClock.advanceTime(100L)
+                    emit(list)
+                }
+            }
+
+            val flowWithLogging =
+                flow.logDiffsForTable(
+                    tableLogBuffer,
+                    COLUMN_PREFIX,
+                    COLUMN_NAME,
+                    initialValue = listOf("val0", "val00"),
+                )
+
+            val job = launch { flowWithLogging.collect() }
+
+            val logs = dumpLog()
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(100L) +
+                        SEPARATOR +
+                        FULL_NAME +
+                        SEPARATOR +
+                        listOf("val0", "val00").toString()
+                )
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(200L) +
+                        SEPARATOR +
+                        FULL_NAME +
+                        SEPARATOR +
+                        listOf("val1", "val2").toString()
+                )
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(300L) +
+                        SEPARATOR +
+                        FULL_NAME +
+                        SEPARATOR +
+                        listOf("val3").toString()
+                )
+            assertThat(logs)
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(400L) +
+                        SEPARATOR +
+                        FULL_NAME +
+                        SEPARATOR +
+                        listOf("val4", "val5", "val6").toString()
+                )
+
+            job.cancel()
+        }
+
+    @Test
+    fun list_doesNotLogIfSameValue() =
+        testScope.runTest {
+            systemClock.setCurrentTimeMillis(100L)
+
+            val listItems =
+                listOf(
+                    listOf("val0", "val00"),
+                    listOf("val1"),
+                    listOf("val1"),
+                    listOf("val1", "val2"),
+                )
+            val flow = flow {
+                for (bool in listItems) {
+                    systemClock.advanceTime(100L)
+                    emit(bool)
+                }
+            }
+
+            val flowWithLogging =
+                flow.logDiffsForTable(
+                    tableLogBuffer,
+                    COLUMN_PREFIX,
+                    COLUMN_NAME,
+                    initialValue = listOf("val0", "val00"),
+                )
+
+            val job = launch { flowWithLogging.collect() }
+
+            val logs = dumpLog()
+
+            val expected1 =
+                TABLE_LOG_DATE_FORMAT.format(100L) +
+                    SEPARATOR +
+                    FULL_NAME +
+                    SEPARATOR +
+                    listOf("val0", "val00").toString()
+            val expected3 =
+                TABLE_LOG_DATE_FORMAT.format(300L) +
+                    SEPARATOR +
+                    FULL_NAME +
+                    SEPARATOR +
+                    listOf("val1").toString()
+            val expected5 =
+                TABLE_LOG_DATE_FORMAT.format(500L) +
+                    SEPARATOR +
+                    FULL_NAME +
+                    SEPARATOR +
+                    listOf("val1", "val2").toString()
+            assertThat(logs).contains(expected1)
+            assertThat(logs).contains(expected3)
+            assertThat(logs).contains(expected5)
+
+            val unexpected2 =
+                TABLE_LOG_DATE_FORMAT.format(200L) +
+                    SEPARATOR +
+                    FULL_NAME +
+                    SEPARATOR +
+                    listOf("val0", "val00")
+            val unexpected4 =
+                TABLE_LOG_DATE_FORMAT.format(400L) +
+                    SEPARATOR +
+                    FULL_NAME +
+                    SEPARATOR +
+                    listOf("val1")
+            assertThat(logs).doesNotContain(unexpected2)
+            assertThat(logs).doesNotContain(unexpected4)
+            job.cancel()
+        }
+
+    @Test
+    fun list_worksForStateFlows() =
+        testScope.runTest {
+            val flow = MutableStateFlow(listOf(1111))
+
+            val flowWithLogging =
+                flow.logDiffsForTable(
+                    tableLogBuffer,
+                    COLUMN_PREFIX,
+                    COLUMN_NAME,
+                    initialValue = listOf(1111),
+                )
+
+            systemClock.setCurrentTimeMillis(50L)
+            val job = launch { flowWithLogging.collect() }
+            assertThat(dumpLog())
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(50L) +
+                        SEPARATOR +
+                        FULL_NAME +
+                        SEPARATOR +
+                        listOf(1111).toString()
+                )
+
+            systemClock.setCurrentTimeMillis(100L)
+            flow.emit(listOf(2222, 3333))
+            assertThat(dumpLog())
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(100L) +
+                        SEPARATOR +
+                        FULL_NAME +
+                        SEPARATOR +
+                        listOf(2222, 3333).toString()
+                )
+
+            systemClock.setCurrentTimeMillis(200L)
+            flow.emit(listOf(3333, 4444))
+            assertThat(dumpLog())
+                .contains(
+                    TABLE_LOG_DATE_FORMAT.format(200L) +
+                        SEPARATOR +
+                        FULL_NAME +
+                        SEPARATOR +
+                        listOf(3333, 4444).toString()
+                )
+
+            // Doesn't log duplicates
+            systemClock.setCurrentTimeMillis(300L)
+            flow.emit(listOf(3333, 4444))
+            assertThat(dumpLog())
+                .doesNotContain(
+                    TABLE_LOG_DATE_FORMAT.format(300L) +
+                        SEPARATOR +
+                        FULL_NAME +
+                        SEPARATOR +
+                        listOf(3333, 4444).toString()
+                )
+
+            job.cancel()
+        }
+
     private fun dumpLog(): String {
         val outputWriter = StringWriter()
         tableLogBuffer.dump(PrintWriter(outputWriter), arrayOf())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt
index 432764a..c7f3fa0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/log/table/TableChangeTest.kt
@@ -36,6 +36,17 @@
     }
 
     @Test
+    fun setString_null() {
+        val underTest = TableChange()
+
+        underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+        underTest.set(null as String?)
+
+        assertThat(underTest.hasData()).isTrue()
+        assertThat(underTest.getVal()).isEqualTo("null")
+    }
+
+    @Test
     fun setBoolean_isBoolean() {
         val underTest = TableChange()
 
@@ -58,6 +69,17 @@
     }
 
     @Test
+    fun setInt_null() {
+        val underTest = TableChange()
+
+        underTest.reset(timestamp = 100, columnPrefix = "", columnName = "fakeName")
+        underTest.set(null as Int?)
+
+        assertThat(underTest.hasData()).isTrue()
+        assertThat(underTest.getVal()).isEqualTo("null")
+    }
+
+    @Test
     fun setThenReset_isEmpty() {
         val underTest = TableChange()
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
index 44e2fbd..6f1b42b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/pipeline/MediaDataManagerTest.kt
@@ -40,6 +40,7 @@
 import androidx.media.utils.MediaConstants
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
+import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.InstanceIdSequenceFake
 import com.android.systemui.R
 import com.android.systemui.SysuiTestCase
@@ -51,6 +52,7 @@
 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaDataProvider
 import com.android.systemui.media.controls.resume.MediaResumeListener
+import com.android.systemui.media.controls.resume.ResumeMediaBrowser
 import com.android.systemui.media.controls.util.MediaControllerFactory
 import com.android.systemui.media.controls.util.MediaFlags
 import com.android.systemui.media.controls.util.MediaUiEventLogger
@@ -125,6 +127,7 @@
     @Mock lateinit var pendingIntent: PendingIntent
     @Mock lateinit var activityStarter: ActivityStarter
     @Mock lateinit var smartspaceManager: SmartspaceManager
+    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
     lateinit var smartspaceMediaDataProvider: SmartspaceMediaDataProvider
     @Mock lateinit var mediaSmartspaceTarget: SmartspaceTarget
     @Mock private lateinit var mediaRecommendationItem: SmartspaceAction
@@ -186,6 +189,7 @@
                 mediaFlags = mediaFlags,
                 logger = logger,
                 smartspaceManager = smartspaceManager,
+                keyguardUpdateMonitor = keyguardUpdateMonitor
             )
         verify(tunerService)
             .addTunable(capture(tunableCaptor), eq(Settings.Secure.MEDIA_CONTROLS_RECOMMENDATION))
@@ -241,6 +245,7 @@
         whenever(mediaFlags.isRetainingPlayersEnabled()).thenReturn(false)
         whenever(mediaFlags.isPersistentSsCardEnabled()).thenReturn(false)
         whenever(logger.getNewInstanceId()).thenReturn(instanceIdSequence.newInstanceId())
+        whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(false)
     }
 
     @After
@@ -642,6 +647,59 @@
     }
 
     @Test
+    fun testOnNotificationRemoved_withResumption_tooManyPlayers() {
+        // Given the maximum number of resume controls already
+        val desc =
+            MediaDescription.Builder().run {
+                setTitle(SESSION_TITLE)
+                build()
+            }
+        for (i in 0..ResumeMediaBrowser.MAX_RESUMPTION_CONTROLS) {
+            addResumeControlAndLoad(desc, "$i:$PACKAGE_NAME")
+            clock.advanceTime(1000)
+        }
+
+        // And an active, resumable notification
+        whenever(controller.metadata).thenReturn(metadataBuilder.build())
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        assertThat(data.resumption).isFalse()
+        mediaDataManager.onMediaDataLoaded(KEY, null, data.copy(resumeAction = Runnable {}))
+
+        // When the notification is removed
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        // Then it is converted to resumption
+        verify(listener)
+            .onMediaDataLoaded(
+                eq(PACKAGE_NAME),
+                eq(KEY),
+                capture(mediaDataCaptor),
+                eq(true),
+                eq(0),
+                eq(false)
+            )
+        assertThat(mediaDataCaptor.value.resumption).isTrue()
+        assertThat(mediaDataCaptor.value.isPlaying).isFalse()
+
+        // And the oldest resume control was removed
+        verify(listener).onMediaDataRemoved(eq("0:$PACKAGE_NAME"))
+    }
+
+    fun testOnNotificationRemoved_lockDownMode() {
+        whenever(keyguardUpdateMonitor.isUserInLockdown(any())).thenReturn(true)
+
+        addNotificationAndLoad()
+        val data = mediaDataCaptor.value
+        mediaDataManager.onNotificationRemoved(KEY)
+
+        verify(listener, never()).onMediaDataRemoved(eq(KEY))
+        verify(logger, never())
+            .logActiveConvertedToResume(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+        verify(logger).logMediaRemoved(anyInt(), eq(PACKAGE_NAME), eq(data.instanceId))
+    }
+
+    @Test
     fun testAddResumptionControls() {
         // WHEN resumption controls are added
         val desc =
@@ -1846,7 +1904,10 @@
     }
 
     /** Helper function to add a resumption control and capture the resulting MediaData */
-    private fun addResumeControlAndLoad(desc: MediaDescription) {
+    private fun addResumeControlAndLoad(
+        desc: MediaDescription,
+        packageName: String = PACKAGE_NAME
+    ) {
         mediaDataManager.addResumptionControls(
             USER_ID,
             desc,
@@ -1854,14 +1915,14 @@
             session.sessionToken,
             APP_NAME,
             pendingIntent,
-            PACKAGE_NAME
+            packageName
         )
         assertThat(backgroundExecutor.runAllReady()).isEqualTo(1)
         assertThat(foregroundExecutor.runAllReady()).isEqualTo(1)
 
         verify(listener)
             .onMediaDataLoaded(
-                eq(PACKAGE_NAME),
+                eq(packageName),
                 eq(null),
                 capture(mediaDataCaptor),
                 eq(true),
diff --git a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
index e201b6b..997198e 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/media/controls/ui/MediaCarouselControllerTest.kt
@@ -21,12 +21,20 @@
 import android.testing.AndroidTestingRunner
 import android.testing.TestableLooper
 import android.util.MathUtils.abs
+import android.view.View
 import androidx.test.filters.SmallTest
 import com.android.internal.logging.InstanceId
+import com.android.keyguard.KeyguardUpdateMonitor
+import com.android.keyguard.KeyguardUpdateMonitorCallback
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.classifier.FalsingCollector
 import com.android.systemui.dagger.qualifiers.Main
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
+import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.TransitionState
+import com.android.systemui.keyguard.shared.model.TransitionStep
 import com.android.systemui.media.controls.MediaTestUtils
 import com.android.systemui.media.controls.models.player.MediaData
 import com.android.systemui.media.controls.models.recommendation.SmartspaceMediaData
@@ -49,6 +57,9 @@
 import junit.framework.Assert.assertEquals
 import junit.framework.Assert.assertFalse
 import junit.framework.Assert.assertTrue
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
 import org.junit.Before
 import org.junit.Ignore
 import org.junit.Test
@@ -90,11 +101,15 @@
     @Mock lateinit var mediaCarousel: MediaScrollView
     @Mock lateinit var pageIndicator: PageIndicator
     @Mock lateinit var mediaFlags: MediaFlags
+    @Mock lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
+    @Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
+    private lateinit var transitionRepository: FakeKeyguardTransitionRepository
     @Captor lateinit var listener: ArgumentCaptor<MediaDataManager.Listener>
     @Captor
     lateinit var configListener: ArgumentCaptor<ConfigurationController.ConfigurationListener>
     @Captor lateinit var newConfig: ArgumentCaptor<Configuration>
     @Captor lateinit var visualStabilityCallback: ArgumentCaptor<OnReorderingAllowedListener>
+    @Captor lateinit var keyguardCallback: ArgumentCaptor<KeyguardUpdateMonitorCallback>
 
     private val clock = FakeSystemClock()
     private lateinit var mediaCarouselController: MediaCarouselController
@@ -102,6 +117,7 @@
     @Before
     fun setup() {
         MockitoAnnotations.initMocks(this)
+        transitionRepository = FakeKeyguardTransitionRepository()
         mediaCarouselController =
             MediaCarouselController(
                 context,
@@ -119,11 +135,14 @@
                 logger,
                 debugLogger,
                 mediaFlags,
+                keyguardUpdateMonitor,
+                KeyguardTransitionInteractor(repository = transitionRepository),
             )
         verify(configurationController).addCallback(capture(configListener))
         verify(mediaDataManager).addListener(capture(listener))
         verify(visualStabilityProvider)
             .addPersistentReorderingAllowedListener(capture(visualStabilityCallback))
+        verify(keyguardUpdateMonitor).registerCallback(capture(keyguardCallback))
         whenever(mediaControlPanelFactory.get()).thenReturn(panel)
         whenever(panel.mediaViewController).thenReturn(mediaViewController)
         whenever(mediaDataManager.smartspaceMediaData).thenReturn(smartspaceMediaData)
@@ -740,4 +759,41 @@
         assertTrue(MediaPlayerData.visiblePlayerKeys().elementAt(0).isSsMediaRec)
         assertFalse(MediaPlayerData.visiblePlayerKeys().elementAt(0).data.active)
     }
+
+    @Test
+    fun testOnLockDownMode_hideMediaCarousel() {
+        whenever(keyguardUpdateMonitor.isUserInLockdown(context.userId)).thenReturn(true)
+        mediaCarouselController.mediaCarousel = mediaCarousel
+
+        keyguardCallback.value.onStrongAuthStateChanged(context.userId)
+
+        verify(mediaCarousel).visibility = View.GONE
+    }
+
+    @Test
+    fun testLockDownModeOff_showMediaCarousel() {
+        whenever(keyguardUpdateMonitor.isUserInLockdown(context.userId)).thenReturn(false)
+        whenever(keyguardUpdateMonitor.isUserUnlocked(context.userId)).thenReturn(true)
+        mediaCarouselController.mediaCarousel = mediaCarousel
+
+        keyguardCallback.value.onStrongAuthStateChanged(context.userId)
+
+        verify(mediaCarousel).visibility = View.VISIBLE
+    }
+
+    @ExperimentalCoroutinesApi
+    @Test
+    fun testKeyguardGone_showMediaCarousel() =
+        runTest(UnconfinedTestDispatcher()) {
+            mediaCarouselController.mediaCarousel = mediaCarousel
+
+            val job = mediaCarouselController.listenForAnyStateToGoneKeyguardTransition(this)
+            transitionRepository.sendTransitionStep(
+                TransitionStep(to = KeyguardState.GONE, transitionState = TransitionState.FINISHED)
+            )
+
+            verify(mediaCarousel).visibility = View.VISIBLE
+
+            job.cancel()
+        }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
index 1042ea7..4977775 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorControllerTest.kt
@@ -24,6 +24,8 @@
     private val taskListProvider = TestRecentTaskListProvider()
     private val scope = CoroutineScope(Dispatchers.Unconfined)
     private val appSelectorComponentName = ComponentName("com.test", "AppSelector")
+    private val callerPackageName = "com.test.caller"
+    private val callerComponentName = ComponentName(callerPackageName, "Caller")
 
     private val hostUserHandle = UserHandle.of(123)
     private val otherUserHandle = UserHandle.of(456)
@@ -31,14 +33,16 @@
     private val view: MediaProjectionAppSelectorView = mock()
     private val featureFlags: FeatureFlags = mock()
 
-    private val controller = MediaProjectionAppSelectorController(
-        taskListProvider,
-        view,
-        featureFlags,
-        hostUserHandle,
-        scope,
-        appSelectorComponentName
-    )
+    private val controller =
+        MediaProjectionAppSelectorController(
+            taskListProvider,
+            view,
+            featureFlags,
+            hostUserHandle,
+            scope,
+            appSelectorComponentName,
+            callerPackageName
+        )
 
     @Test
     fun initNoRecentTasks_bindsEmptyList() {
@@ -51,104 +55,87 @@
 
     @Test
     fun initOneRecentTask_bindsList() {
-        taskListProvider.tasks = listOf(
-            createRecentTask(taskId = 1)
-        )
+        taskListProvider.tasks = listOf(createRecentTask(taskId = 1))
 
         controller.init()
 
-        verify(view).bind(
-            listOf(
-                createRecentTask(taskId = 1)
-            )
-        )
+        verify(view).bind(listOf(createRecentTask(taskId = 1)))
     }
 
     @Test
     fun initMultipleRecentTasksWithoutAppSelectorTask_bindsListInTheSameOrder() {
-        val tasks = listOf(
-            createRecentTask(taskId = 1),
-            createRecentTask(taskId = 2),
-            createRecentTask(taskId = 3),
-        )
-        taskListProvider.tasks = tasks
-
-        controller.init()
-
-        verify(view).bind(
+        val tasks =
             listOf(
                 createRecentTask(taskId = 1),
                 createRecentTask(taskId = 2),
                 createRecentTask(taskId = 3),
             )
-        )
-    }
-
-    @Test
-    fun initRecentTasksWithAppSelectorTasks_bindsAppSelectorTasksAtTheEnd() {
-        val tasks = listOf(
-            createRecentTask(taskId = 1),
-            createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName),
-            createRecentTask(taskId = 3),
-            createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName),
-            createRecentTask(taskId = 5),
-        )
         taskListProvider.tasks = tasks
 
         controller.init()
 
-        verify(view).bind(
+        verify(view)
+            .bind(
+                listOf(
+                    createRecentTask(taskId = 1),
+                    createRecentTask(taskId = 2),
+                    createRecentTask(taskId = 3),
+                )
+            )
+    }
+
+    @Test
+    fun initRecentTasksWithAppSelectorTasks_removeAppSelector() {
+        val tasks =
             listOf(
                 createRecentTask(taskId = 1),
-                createRecentTask(taskId = 3),
-                createRecentTask(taskId = 5),
                 createRecentTask(taskId = 2, topActivityComponent = appSelectorComponentName),
-                createRecentTask(taskId = 4, topActivityComponent = appSelectorComponentName),
+                createRecentTask(taskId = 3),
+                createRecentTask(taskId = 4),
             )
-        )
+        taskListProvider.tasks = tasks
+
+        controller.init()
+
+        verify(view)
+            .bind(
+                listOf(
+                    createRecentTask(taskId = 1),
+                    createRecentTask(taskId = 3),
+                    createRecentTask(taskId = 4),
+                )
+            )
+    }
+
+    @Test
+    fun initRecentTasksWithAppSelectorTasks_bindsCallerTasksAtTheEnd() {
+        val tasks =
+            listOf(
+                createRecentTask(taskId = 1),
+                createRecentTask(taskId = 2, topActivityComponent = callerComponentName),
+                createRecentTask(taskId = 3),
+                createRecentTask(taskId = 4),
+            )
+        taskListProvider.tasks = tasks
+
+        controller.init()
+
+        verify(view)
+            .bind(
+                listOf(
+                    createRecentTask(taskId = 1),
+                    createRecentTask(taskId = 3),
+                    createRecentTask(taskId = 4),
+                    createRecentTask(taskId = 2, topActivityComponent = callerComponentName),
+                )
+            )
     }
 
     @Test
     fun initRecentTasksWithAppSelectorTasks_enterprisePoliciesDisabled_bindsOnlyTasksWithHostProfile() {
         givenEnterprisePoliciesFeatureFlag(enabled = false)
 
-        val tasks = listOf(
-            createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
-            createRecentTask(taskId = 2, userId = otherUserHandle.identifier),
-            createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
-            createRecentTask(taskId = 4, userId = otherUserHandle.identifier),
-            createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
-        )
-        taskListProvider.tasks = tasks
-
-        controller.init()
-
-        verify(view).bind(
-            listOf(
-                createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
-                createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
-                createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
-            )
-        )
-    }
-
-    @Test
-    fun initRecentTasksWithAppSelectorTasks_enterprisePoliciesEnabled_bindsAllTasks() {
-        givenEnterprisePoliciesFeatureFlag(enabled = true)
-
-        val tasks = listOf(
-            createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
-            createRecentTask(taskId = 2, userId = otherUserHandle.identifier),
-            createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
-            createRecentTask(taskId = 4, userId = otherUserHandle.identifier),
-            createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
-        )
-        taskListProvider.tasks = tasks
-
-        controller.init()
-
-        // TODO(b/233348916) should filter depending on the policies
-        verify(view).bind(
+        val tasks =
             listOf(
                 createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
                 createRecentTask(taskId = 2, userId = otherUserHandle.identifier),
@@ -156,7 +143,47 @@
                 createRecentTask(taskId = 4, userId = otherUserHandle.identifier),
                 createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
             )
-        )
+        taskListProvider.tasks = tasks
+
+        controller.init()
+
+        verify(view)
+            .bind(
+                listOf(
+                    createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
+                    createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
+                    createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
+                )
+            )
+    }
+
+    @Test
+    fun initRecentTasksWithAppSelectorTasks_enterprisePoliciesEnabled_bindsAllTasks() {
+        givenEnterprisePoliciesFeatureFlag(enabled = true)
+
+        val tasks =
+            listOf(
+                createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
+                createRecentTask(taskId = 2, userId = otherUserHandle.identifier),
+                createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
+                createRecentTask(taskId = 4, userId = otherUserHandle.identifier),
+                createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
+            )
+        taskListProvider.tasks = tasks
+
+        controller.init()
+
+        // TODO(b/233348916) should filter depending on the policies
+        verify(view)
+            .bind(
+                listOf(
+                    createRecentTask(taskId = 1, userId = hostUserHandle.identifier),
+                    createRecentTask(taskId = 2, userId = otherUserHandle.identifier),
+                    createRecentTask(taskId = 3, userId = hostUserHandle.identifier),
+                    createRecentTask(taskId = 4, userId = otherUserHandle.identifier),
+                    createRecentTask(taskId = 5, userId = hostUserHandle.identifier),
+                )
+            )
     }
 
     private fun givenEnterprisePoliciesFeatureFlag(enabled: Boolean) {
@@ -183,6 +210,5 @@
         var tasks: List<RecentTask> = emptyList()
 
         override suspend fun loadRecentTasks(): List<RecentTask> = tasks
-
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/process/condition/UserProcessConditionTest.java b/packages/SystemUI/tests/src/com/android/systemui/process/condition/UserProcessConditionTest.java
new file mode 100644
index 0000000..2293fc5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/process/condition/UserProcessConditionTest.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 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.process.condition;
+
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.process.ProcessWrapper;
+import com.android.systemui.settings.UserTracker;
+import com.android.systemui.shared.condition.Condition;
+import com.android.systemui.shared.condition.Monitor;
+import com.android.systemui.util.concurrency.FakeExecutor;
+import com.android.systemui.util.time.FakeSystemClock;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class UserProcessConditionTest extends SysuiTestCase {
+    @Mock
+    UserTracker mUserTracker;
+
+    @Mock
+    ProcessWrapper mProcessWrapper;
+
+    @Mock
+    Monitor.Callback mCallback;
+
+    private final FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock());
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+    }
+
+    /**
+     * Verifies condition reports false when tracker reports a different user id than the
+     * identifier from the process handle.
+     */
+    @Test
+    public void testConditionFailsWithDifferentIds() {
+
+        final Condition condition = new UserProcessCondition(mProcessWrapper, mUserTracker);
+        when(mProcessWrapper.getUserHandleIdentifier()).thenReturn(0);
+        when(mUserTracker.getUserId()).thenReturn(1);
+
+        final Monitor monitor = new Monitor(mExecutor);
+
+        monitor.addSubscription(new Monitor.Subscription.Builder(mCallback)
+                .addCondition(condition)
+                .build());
+
+        mExecutor.runAllReady();
+
+        verify(mCallback).onConditionsChanged(false);
+    }
+
+    /**
+     * Verifies condition reports false when tracker reports a different user id than the
+     * identifier from the process handle.
+     */
+    @Test
+    public void testConditionSucceedsWithSameIds() {
+
+        final Condition condition = new UserProcessCondition(mProcessWrapper, mUserTracker);
+        when(mProcessWrapper.getUserHandleIdentifier()).thenReturn(0);
+        when(mUserTracker.getUserId()).thenReturn(0);
+
+        final Monitor monitor = new Monitor(mExecutor);
+
+        monitor.addSubscription(new Monitor.Subscription.Builder(mCallback)
+                .addCondition(condition)
+                .build());
+
+        mExecutor.runAllReady();
+
+        verify(mCallback).onConditionsChanged(true);
+    }
+
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java
index 7693fee..9eccbb6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shared/condition/ConditionMonitorTest.java
@@ -471,4 +471,142 @@
         mExecutor.runAllReady();
         verify(callback).onConditionsChanged(true);
     }
+
+    /**
+     * Ensures that the result of a condition being true leads to its nested condition being
+     * activated.
+     */
+    @Test
+    public void testNestedCondition() {
+        mCondition1.fakeUpdateCondition(false);
+        final Monitor.Callback callback =
+                mock(Monitor.Callback.class);
+
+        mCondition2.fakeUpdateCondition(false);
+
+        // Create a nested condition
+        mConditionMonitor.addSubscription(new Monitor.Subscription.Builder(
+                new Monitor.Subscription.Builder(callback)
+                        .addCondition(mCondition2)
+                        .build())
+                .addCondition(mCondition1)
+                .build());
+
+        mExecutor.runAllReady();
+
+        // Ensure the nested condition callback is not called at all.
+        verify(callback, never()).onActiveChanged(anyBoolean());
+        verify(callback, never()).onConditionsChanged(anyBoolean());
+
+        // Update the inner condition to true and ensure that the nested condition is not triggered.
+        mCondition2.fakeUpdateCondition(true);
+        verify(callback, never()).onConditionsChanged(anyBoolean());
+        mCondition2.fakeUpdateCondition(false);
+
+        // Set outer condition and make sure the inner condition becomes active and reports that
+        // conditions aren't met
+        mCondition1.fakeUpdateCondition(true);
+        mExecutor.runAllReady();
+
+        verify(callback).onActiveChanged(eq(true));
+        verify(callback).onConditionsChanged(eq(false));
+
+        Mockito.clearInvocations(callback);
+
+        // Update the inner condition and make sure the callback is updated.
+        mCondition2.fakeUpdateCondition(true);
+        mExecutor.runAllReady();
+
+        verify(callback).onConditionsChanged(true);
+
+        Mockito.clearInvocations(callback);
+        // Invalidate outer condition and make sure callback is informed, but the last state is
+        // not affected.
+        mCondition1.fakeUpdateCondition(false);
+        mExecutor.runAllReady();
+
+        verify(callback).onActiveChanged(eq(false));
+        verify(callback, never()).onConditionsChanged(anyBoolean());
+    }
+
+    /**
+     * Ensures a subscription is predicated on its precondition.
+     */
+    @Test
+    public void testPrecondition() {
+        mCondition1.fakeUpdateCondition(false);
+        final Monitor.Callback callback =
+                mock(Monitor.Callback.class);
+
+        mCondition2.fakeUpdateCondition(false);
+
+        // Create a nested condition
+        mConditionMonitor.addSubscription(new Monitor.Subscription.Builder(callback)
+                .addPrecondition(mCondition1)
+                .addCondition(mCondition2)
+                .build());
+
+        mExecutor.runAllReady();
+
+        // Ensure the nested condition callback is not called at all.
+        verify(callback, never()).onActiveChanged(anyBoolean());
+        verify(callback, never()).onConditionsChanged(anyBoolean());
+
+        // Update the condition to true and ensure that the nested condition is not triggered.
+        mCondition2.fakeUpdateCondition(true);
+        verify(callback, never()).onConditionsChanged(anyBoolean());
+        mCondition2.fakeUpdateCondition(false);
+
+        // Set precondition and make sure the inner condition becomes active and reports that
+        // conditions aren't met
+        mCondition1.fakeUpdateCondition(true);
+        mExecutor.runAllReady();
+
+        verify(callback).onActiveChanged(eq(true));
+        verify(callback).onConditionsChanged(eq(false));
+
+        Mockito.clearInvocations(callback);
+
+        // Update the condition and make sure the callback is updated.
+        mCondition2.fakeUpdateCondition(true);
+        mExecutor.runAllReady();
+
+        verify(callback).onConditionsChanged(true);
+
+        Mockito.clearInvocations(callback);
+        // Invalidate precondition and make sure callback is informed, but the last state is
+        // not affected.
+        mCondition1.fakeUpdateCondition(false);
+        mExecutor.runAllReady();
+
+        verify(callback).onActiveChanged(eq(false));
+        verify(callback, never()).onConditionsChanged(anyBoolean());
+    }
+
+    /**
+     * Ensure preconditions are applied to every subscription added to a monitor.
+     */
+    @Test
+    public void testPreconditionMonitor() {
+        final Monitor.Callback callback =
+                mock(Monitor.Callback.class);
+
+        mCondition2.fakeUpdateCondition(true);
+        final Monitor monitor = new Monitor(mExecutor, new HashSet<>(Arrays.asList(mCondition1)));
+
+        monitor.addSubscription(new Monitor.Subscription.Builder(callback)
+                .addCondition(mCondition2)
+                .build());
+
+        mExecutor.runAllReady();
+
+        verify(callback, never()).onActiveChanged(anyBoolean());
+        verify(callback, never()).onConditionsChanged(anyBoolean());
+
+        mCondition1.fakeUpdateCondition(true);
+        mExecutor.runAllReady();
+
+        verify(callback).onActiveChanged(eq(true));
+        verify(callback).onConditionsChanged(eq(true));
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index f9f2c45..406826b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -23,6 +23,7 @@
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT;
 import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_TIMEOUT;
 
+import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_AVAILABLE;
 import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
 import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED;
 import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ALIGNMENT;
@@ -620,6 +621,33 @@
     }
 
     @Test
+    public void onBiometricHelp_coEx_faceUnavailable() {
+        createController();
+
+        // GIVEN unlocking with fingerprint is possible
+        when(mKeyguardUpdateMonitor.getCachedIsUnlockWithFingerprintPossible(anyInt()))
+                .thenReturn(true);
+
+        String message = "A message";
+        mController.setVisible(true);
+
+        // WHEN there's a face unavailable message
+        mController.getKeyguardCallback().onBiometricHelp(
+                BIOMETRIC_HELP_FACE_NOT_AVAILABLE,
+                message,
+                BiometricSourceType.FACE);
+
+        // THEN show sequential messages such as: 'face unlock unavailable' and
+        // 'try fingerprint instead'
+        verifyIndicationMessage(
+                INDICATION_TYPE_BIOMETRIC_MESSAGE,
+                message);
+        verifyIndicationMessage(
+                INDICATION_TYPE_BIOMETRIC_MESSAGE_FOLLOW_UP,
+                mContext.getString(R.string.keyguard_suggest_fingerprint));
+    }
+
+    @Test
     public void onBiometricHelp_coEx_fpFailure_faceAlreadyUnlocked() {
         createController();
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
index 9c69a6a..d6225c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceControllerTest.kt
@@ -34,6 +34,7 @@
 import androidx.test.filters.SmallTest
 import com.android.keyguard.KeyguardUpdateMonitor
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
 import com.android.systemui.flags.FeatureFlags
 import com.android.systemui.flags.Flags
 import com.android.systemui.plugins.ActivityStarter
@@ -129,6 +130,9 @@
     private lateinit var configPlugin: BcSmartspaceConfigPlugin
 
     @Mock
+    private lateinit var dumpManager: DumpManager
+
+    @Mock
     private lateinit var controllerListener: SmartspaceTargetListener
 
     @Captor
@@ -228,6 +232,7 @@
                 deviceProvisionedController,
                 keyguardBypassController,
                 keyguardUpdateMonitor,
+                dumpManager,
                 execution,
                 executor,
                 bgExecutor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
index 2686238..49da848 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/KeyguardCoordinatorTest.kt
@@ -38,6 +38,8 @@
 import com.android.systemui.statusbar.notification.collection.provider.SeenNotificationsProviderImpl
 import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.policy.HeadsUpManager
+import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.withArgCaptor
@@ -63,6 +65,7 @@
 @RunWith(AndroidTestingRunner::class)
 class KeyguardCoordinatorTest : SysuiTestCase() {
 
+    private val headsUpManager: HeadsUpManager = mock()
     private val keyguardNotifVisibilityProvider: KeyguardNotificationVisibilityProvider = mock()
     private val keyguardRepository = FakeKeyguardRepository()
     private val notifPipelineFlags: NotifPipelineFlags = mock()
@@ -90,8 +93,9 @@
     fun unseenFilterSuppressesSeenNotifWhileKeyguardShowing() {
         whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
 
-        // GIVEN: Keyguard is not showing, and a notification is present
+        // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
         keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
         runKeyguardCoordinatorTest {
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
@@ -113,11 +117,44 @@
     }
 
     @Test
+    fun unseenFilter_headsUpMarkedAsSeen() {
+        whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
+
+        // GIVEN: Keyguard is not showing, shade is not expanded
+        keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(false)
+        runKeyguardCoordinatorTest {
+            // WHEN: A notification is posted
+            val fakeEntry = NotificationEntryBuilder().build()
+            collectionListener.onEntryAdded(fakeEntry)
+
+            // WHEN: That notification is heads up
+            onHeadsUpChangedListener.onHeadsUpStateChanged(fakeEntry, /* isHeadsUp= */ true)
+            testScheduler.runCurrent()
+
+            // WHEN: The keyguard is now showing
+            keyguardRepository.setKeyguardShowing(true)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is recognized as "seen" and is filtered out.
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isTrue()
+
+            // WHEN: The keyguard goes away
+            keyguardRepository.setKeyguardShowing(false)
+            testScheduler.runCurrent()
+
+            // THEN: The notification is shown regardless
+            assertThat(unseenFilter.shouldFilterOut(fakeEntry, 0L)).isFalse()
+        }
+    }
+
+    @Test
     fun unseenFilterDoesNotSuppressSeenOngoingNotifWhileKeyguardShowing() {
         whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
 
-        // GIVEN: Keyguard is not showing, and an ongoing notification is present
+        // GIVEN: Keyguard is not showing, shade is expanded, and an ongoing notification is present
         keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
         runKeyguardCoordinatorTest {
             val fakeEntry = NotificationEntryBuilder()
                 .setNotification(Notification.Builder(mContext).setOngoing(true).build())
@@ -137,8 +174,9 @@
     fun unseenFilterDoesNotSuppressSeenMediaNotifWhileKeyguardShowing() {
         whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
 
-        // GIVEN: Keyguard is not showing, and a media notification is present
+        // GIVEN: Keyguard is not showing, shade is expanded, and a media notification is present
         keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
         runKeyguardCoordinatorTest {
             val fakeEntry = NotificationEntryBuilder().build().apply {
                 row = mock<ExpandableNotificationRow>().apply {
@@ -160,8 +198,9 @@
     fun unseenFilterUpdatesSeenProviderWhenSuppressing() {
         whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
 
-        // GIVEN: Keyguard is not showing, and a notification is present
+        // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
         keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
         runKeyguardCoordinatorTest {
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
@@ -185,8 +224,9 @@
     fun unseenFilterInvalidatesWhenSettingChanges() {
         whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
 
-        // GIVEN: Keyguard is not showing
+        // GIVEN: Keyguard is not showing, and shade is expanded
         keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
         runKeyguardCoordinatorTest {
             // GIVEN: A notification is present
             val fakeEntry = NotificationEntryBuilder().build()
@@ -237,8 +277,9 @@
     fun unseenFilterSeenGroupSummaryWithUnseenChild() {
         whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
 
-        // GIVEN: Keyguard is not showing, and a notification is present
+        // GIVEN: Keyguard is not showing, shade is expanded, and a notification is present
         keyguardRepository.setKeyguardShowing(false)
+        whenever(statusBarStateController.isExpanded).thenReturn(true)
         runKeyguardCoordinatorTest {
             // WHEN: A new notification is posted
             val fakeSummary = NotificationEntryBuilder().build()
@@ -276,11 +317,11 @@
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
 
-            // WHEN: Keyguard is no longer showing for 5 seconds
+            // WHEN: Keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
-            testScheduler.runCurrent()
-            testScheduler.advanceTimeBy(5.seconds.inWholeMilliseconds)
-            testScheduler.runCurrent()
+
+            // When: Shade is expanded
+            statusBarStateListener.onExpandedChanged(true)
 
             // WHEN: Keyguard is shown again
             keyguardRepository.setKeyguardShowing(true)
@@ -292,7 +333,7 @@
     }
 
     @Test
-    fun unseenNotificationIsNotMarkedAsSeenIfTimeThresholdNotMet() {
+    fun unseenNotificationIsNotMarkedAsSeenIfShadeNotExpanded() {
         whenever(notifPipelineFlags.shouldFilterUnseenNotifsOnKeyguard).thenReturn(true)
 
         // GIVEN: Keyguard is showing, unseen notification is present
@@ -301,10 +342,8 @@
             val fakeEntry = NotificationEntryBuilder().build()
             collectionListener.onEntryAdded(fakeEntry)
 
-            // WHEN: Keyguard is no longer showing for <5 seconds
+            // WHEN: Keyguard is no longer showing
             keyguardRepository.setKeyguardShowing(false)
-            testScheduler.runCurrent()
-            testScheduler.advanceTimeBy(1.seconds.inWholeMilliseconds)
 
             // WHEN: Keyguard is shown again
             keyguardRepository.setKeyguardShowing(true)
@@ -327,6 +366,7 @@
         val keyguardCoordinator =
             KeyguardCoordinator(
                 testDispatcher,
+                headsUpManager,
                 keyguardNotifVisibilityProvider,
                 keyguardRepository,
                 notifPipelineFlags,
@@ -364,12 +404,21 @@
         val unseenFilter: NotifFilter
             get() = keyguardCoordinator.unseenNotifFilter
 
-        // TODO(254647461): Remove lazy once Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD is enabled and
-        //  removed
+        // TODO(254647461): Remove lazy from these properties once
+        //    Flags.FILTER_UNSEEN_NOTIFS_ON_KEYGUARD is enabled and removed
+
         val collectionListener: NotifCollectionListener by lazy {
             withArgCaptor { verify(notifPipeline).addCollectionListener(capture()) }
         }
 
+        val onHeadsUpChangedListener: OnHeadsUpChangedListener by lazy {
+            withArgCaptor { verify(headsUpManager).addListener(capture()) }
+        }
+
+        val statusBarStateListener: StatusBarStateController.StateListener by lazy {
+            withArgCaptor { verify(statusBarStateController).addCallback(capture()) }
+        }
+
         var showOnlyUnseenNotifsOnKeyguardSetting: Boolean
             get() =
                 fakeSettings.getIntForUser(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
index 5394d88..3face35 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java
@@ -24,6 +24,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
@@ -42,6 +43,7 @@
 import android.testing.AndroidTestingRunner;
 import android.testing.TestableLooper;
 import android.testing.TestableLooper.RunWithLooper;
+import android.util.TypedValue;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.RemoteViews;
@@ -332,6 +334,38 @@
                 eq(FLAG_CONTENT_VIEW_HEADS_UP));
     }
 
+    @Test
+    public void testNotificationViewHeightTooSmallFailsValidation() {
+        View view = mock(View.class);
+        when(view.getHeight())
+                .thenReturn((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10,
+                        mContext.getResources().getDisplayMetrics()));
+        String result = NotificationContentInflater.isValidView(view, mRow.getEntry(),
+                mContext.getResources());
+        assertNotNull(result);
+    }
+
+    @Test
+    public void testNotificationViewPassesValidation() {
+        View view = mock(View.class);
+        when(view.getHeight())
+                .thenReturn((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 17,
+                        mContext.getResources().getDisplayMetrics()));
+        String result = NotificationContentInflater.isValidView(view, mRow.getEntry(),
+                mContext.getResources());
+        assertNull(result);
+    }
+
+    @Test
+    public void testInvalidNotificationDoesNotInvokeCallback() throws Exception {
+        mRow.getPrivateLayout().removeAllViews();
+        mRow.getEntry().getSbn().getNotification().contentView =
+                new RemoteViews(mContext.getPackageName(), R.layout.invalid_notification_height);
+        inflateAndWait(true, mNotificationInflater, FLAG_CONTENT_VIEW_ALL, mRow);
+        assertEquals(0, mRow.getPrivateLayout().getChildCount());
+        verify(mRow, times(0)).onNotificationUpdated();
+    }
+
     private static void inflateAndWait(NotificationContentInflater inflater,
             @InflationFlag int contentToInflate,
             ExpandableNotificationRow row)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
index 8cfcc07..f568547 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/AutoTileManagerTest.java
@@ -17,6 +17,7 @@
 package com.android.systemui.statusbar.phone;
 
 import static com.android.systemui.qs.dagger.QSFlagsModule.RBC_AVAILABLE;
+import static com.android.systemui.statusbar.phone.AutoTileManager.DEVICE_CONTROLS;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -50,6 +51,7 @@
 
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.qs.QSTile;
 import com.android.systemui.qs.AutoAddTracker;
 import com.android.systemui.qs.QSTileHost;
 import com.android.systemui.qs.ReduceBrightColorsController;
@@ -70,6 +72,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
 import org.mockito.Mock;
 import org.mockito.Mockito;
@@ -77,6 +80,7 @@
 import org.mockito.Spy;
 import org.mockito.stubbing.Answer;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 
@@ -543,6 +547,61 @@
     }
 
     @Test
+    public void testAddControlsTileIfNotPresent() {
+        String spec = DEVICE_CONTROLS;
+        when(mAutoAddTracker.isAdded(eq(spec))).thenReturn(false);
+        when(mQsTileHost.getTiles()).thenReturn(new ArrayList<>());
+
+        mAutoTileManager.init();
+        ArgumentCaptor<DeviceControlsController.Callback> captor =
+                ArgumentCaptor.forClass(DeviceControlsController.Callback.class);
+
+        verify(mDeviceControlsController).setCallback(captor.capture());
+
+        captor.getValue().onControlsUpdate(3);
+        verify(mQsTileHost).addTile(spec, 3);
+        verify(mAutoAddTracker).setTileAdded(spec);
+    }
+
+    @Test
+    public void testDontAddControlsTileIfPresent() {
+        String spec = DEVICE_CONTROLS;
+        when(mAutoAddTracker.isAdded(eq(spec))).thenReturn(false);
+        when(mQsTileHost.getTiles()).thenReturn(new ArrayList<>());
+
+        mAutoTileManager.init();
+        ArgumentCaptor<DeviceControlsController.Callback> captor =
+                ArgumentCaptor.forClass(DeviceControlsController.Callback.class);
+
+        verify(mDeviceControlsController).setCallback(captor.capture());
+
+        captor.getValue().removeControlsAutoTracker();
+        verify(mQsTileHost, never()).addTile(spec, 3);
+        verify(mAutoAddTracker, never()).setTileAdded(spec);
+        verify(mAutoAddTracker).setTileRemoved(spec);
+    }
+
+    @Test
+    public void testRemoveControlsTileFromTrackerWhenRequested() {
+        String spec = "controls";
+        when(mAutoAddTracker.isAdded(eq(spec))).thenReturn(true);
+        QSTile mockTile = mock(QSTile.class);
+        when(mockTile.getTileSpec()).thenReturn(spec);
+        when(mQsTileHost.getTiles()).thenReturn(List.of(mockTile));
+
+        mAutoTileManager.init();
+        ArgumentCaptor<DeviceControlsController.Callback> captor =
+                ArgumentCaptor.forClass(DeviceControlsController.Callback.class);
+
+        verify(mDeviceControlsController).setCallback(captor.capture());
+
+        captor.getValue().onControlsUpdate(3);
+        verify(mQsTileHost, never()).addTile(spec, 3);
+        verify(mAutoAddTracker, never()).setTileAdded(spec);
+    }
+
+
+    @Test
     public void testEmptyArray_doesNotCrash() {
         mContext.getOrCreateTestableResources().addOverride(
                 R.array.config_quickSettingsAutoAdd, new String[0]);
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 6fb6893..8aaa57f 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
@@ -23,6 +23,8 @@
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.testing.AndroidTestingRunner;
@@ -33,7 +35,10 @@
 import androidx.test.filters.SmallTest;
 
 import com.android.internal.statusbar.StatusBarIcon;
+import com.android.systemui.demomode.DemoModeController;
+import com.android.systemui.dump.DumpManager;
 import com.android.systemui.plugins.DarkIconDispatcher;
+import com.android.systemui.statusbar.CommandQueue;
 import com.android.systemui.statusbar.StatusBarIconView;
 import com.android.systemui.statusbar.StatusBarMobileView;
 import com.android.systemui.statusbar.StatusBarWifiView;
@@ -46,6 +51,8 @@
 import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags;
 import com.android.systemui.statusbar.pipeline.mobile.ui.MobileUiAdapter;
 import com.android.systemui.statusbar.pipeline.wifi.ui.WifiUiAdapter;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.tuner.TunerService;
 import com.android.systemui.utils.leaks.LeakCheckedTest;
 
 import org.junit.Before;
@@ -87,6 +94,61 @@
         testCallOnAdd_forManager(manager);
     }
 
+    @Test
+    public void testRemoveIcon_ignoredForNewPipeline() {
+        IconManager manager = mock(IconManager.class);
+
+        // GIVEN the new pipeline is on
+        StatusBarPipelineFlags flags = mock(StatusBarPipelineFlags.class);
+        when(flags.isIconControlledByFlags("test_icon")).thenReturn(true);
+
+        StatusBarIconController iconController = new StatusBarIconControllerImpl(
+                mContext,
+                mock(CommandQueue.class),
+                mock(DemoModeController.class),
+                mock(ConfigurationController.class),
+                mock(TunerService.class),
+                mock(DumpManager.class),
+                mock(StatusBarIconList.class),
+                flags
+        );
+
+        iconController.addIconGroup(manager);
+
+        // WHEN a request to remove a new icon is sent
+        iconController.removeIcon("test_icon", 0);
+
+        // THEN it is not removed for those icons
+        verify(manager, never()).onRemoveIcon(anyInt());
+    }
+
+    @Test
+    public void testRemoveAllIconsForSlot_ignoredForNewPipeline() {
+        IconManager manager = mock(IconManager.class);
+
+        // GIVEN the new pipeline is on
+        StatusBarPipelineFlags flags = mock(StatusBarPipelineFlags.class);
+        when(flags.isIconControlledByFlags("test_icon")).thenReturn(true);
+
+        StatusBarIconController iconController = new StatusBarIconControllerImpl(
+                mContext,
+                mock(CommandQueue.class),
+                mock(DemoModeController.class),
+                mock(ConfigurationController.class),
+                mock(TunerService.class),
+                mock(DumpManager.class),
+                mock(StatusBarIconList.class),
+                flags
+        );
+
+        iconController.addIconGroup(manager);
+
+        // WHEN a request to remove a new icon is sent
+        iconController.removeAllIconsForSlot("test_icon");
+
+        // THEN it is not removed for those icons
+        verify(manager, never()).onRemoveIcon(anyInt());
+    }
 
     private <T extends IconManager & TestableIconManager> void testCallOnAdd_forManager(T manager) {
         StatusBarIconHolder holder = holderForType(TYPE_ICON);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt
new file mode 100644
index 0000000..63cb30c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/model/SystemUiCarrierConfigTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2023 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.mobile.data.model
+
+import android.os.PersistableBundle
+import android.telephony.CarrierConfigManager
+import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL
+import android.telephony.CarrierConfigManager.KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Before
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class SystemUiCarrierConfigTest : SysuiTestCase() {
+
+    lateinit var underTest: SystemUiCarrierConfig
+
+    @Before
+    fun setUp() {
+        underTest = SystemUiCarrierConfig(SUB_1_ID, createTestConfig())
+    }
+
+    @Test
+    fun `process new config - reflected by isUsingDefault`() {
+        // Starts out using the defaults
+        assertThat(underTest.isUsingDefault).isTrue()
+
+        // ANY new config means we're no longer tracking defaults
+        underTest.processNewCarrierConfig(createTestConfig())
+
+        assertThat(underTest.isUsingDefault).isFalse()
+    }
+
+    @Test
+    fun `process new config - updates all flows`() {
+        assertThat(underTest.shouldInflateSignalStrength.value).isFalse()
+        assertThat(underTest.showOperatorNameInStatusBar.value).isFalse()
+
+        underTest.processNewCarrierConfig(
+            configWithOverrides(
+                KEY_INFLATE_SIGNAL_STRENGTH_BOOL to true,
+                KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL to true,
+            )
+        )
+
+        assertThat(underTest.shouldInflateSignalStrength.value).isTrue()
+        assertThat(underTest.showOperatorNameInStatusBar.value).isTrue()
+    }
+
+    @Test
+    fun `process new config - defaults to false for config overrides`() {
+        // This case is only apparent when:
+        //   1. The default is true
+        //   2. The override config has no value for a given key
+        // In this case (per the old code) we would use the default value of false, despite there
+        // being no override key present in the override config
+
+        underTest =
+            SystemUiCarrierConfig(
+                SUB_1_ID,
+                configWithOverrides(
+                    KEY_INFLATE_SIGNAL_STRENGTH_BOOL to true,
+                    KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL to true,
+                )
+            )
+
+        assertThat(underTest.isUsingDefault).isTrue()
+        assertThat(underTest.shouldInflateSignalStrength.value).isTrue()
+        assertThat(underTest.showOperatorNameInStatusBar.value).isTrue()
+
+        // Process a new config with no keys
+        underTest.processNewCarrierConfig(PersistableBundle())
+
+        assertThat(underTest.isUsingDefault).isFalse()
+        assertThat(underTest.shouldInflateSignalStrength.value).isFalse()
+        assertThat(underTest.showOperatorNameInStatusBar.value).isFalse()
+    }
+
+    companion object {
+        private const val SUB_1_ID = 1
+
+        /**
+         * In order to keep us from having to update every place that might want to create a config,
+         * make sure to add new keys here
+         */
+        fun createTestConfig() =
+            PersistableBundle().also {
+                it.putBoolean(CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false)
+                it.putBoolean(CarrierConfigManager.KEY_SHOW_OPERATOR_NAME_IN_STATUSBAR_BOOL, false)
+            }
+
+        /** Override the default config with the given (key, value) pair */
+        fun configWithOverride(key: String, override: Boolean): PersistableBundle =
+            createTestConfig().also { it.putBoolean(key, override) }
+
+        /** Override any number of configs from the default */
+        fun configWithOverrides(vararg overrides: Pair<String, Boolean>) =
+            createTestConfig().also { config ->
+                overrides.forEach { (key, value) -> config.putBoolean(key, value) }
+            }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt
new file mode 100644
index 0000000..521c67f
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/CarrierConfigRepositoryTest.kt
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2023 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.mobile.data.repository
+
+import android.content.Intent
+import android.os.PersistableBundle
+import android.telephony.CarrierConfigManager
+import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.createTestConfig
+import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+import org.mockito.MockitoSession
+import org.mockito.quality.Strictness
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class CarrierConfigRepositoryTest : SysuiTestCase() {
+    private val testDispatcher = UnconfinedTestDispatcher()
+    private val testScope = TestScope(testDispatcher)
+
+    private lateinit var underTest: CarrierConfigRepository
+    private lateinit var mockitoSession: MockitoSession
+    private lateinit var carrierConfigCoreStartable: CarrierConfigCoreStartable
+
+    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var carrierConfigManager: CarrierConfigManager
+    @Mock private lateinit var dumpManager: DumpManager
+
+    @Before
+    fun setUp() {
+        MockitoAnnotations.initMocks(this)
+        mockitoSession =
+            mockitoSession()
+                .initMocks(this)
+                .mockStatic(CarrierConfigManager::class.java)
+                .strictness(Strictness.LENIENT)
+                .startMocking()
+
+        whenever(CarrierConfigManager.getDefaultConfig()).thenReturn(DEFAULT_CONFIG)
+
+        whenever(carrierConfigManager.getConfigForSubId(anyInt())).thenAnswer { invocation ->
+            when (invocation.getArgument(0) as Int) {
+                1 -> CONFIG_1
+                2 -> CONFIG_2
+                else -> null
+            }
+        }
+
+        underTest =
+            CarrierConfigRepository(
+                fakeBroadcastDispatcher,
+                carrierConfigManager,
+                dumpManager,
+                logger,
+                testScope.backgroundScope,
+            )
+
+        carrierConfigCoreStartable =
+            CarrierConfigCoreStartable(underTest, testScope.backgroundScope)
+    }
+
+    @After
+    fun tearDown() {
+        mockitoSession.finishMocking()
+    }
+
+    @Test
+    fun `carrier config stream produces int-bundle pairs`() =
+        testScope.runTest {
+            var latest: Pair<Int, PersistableBundle>? = null
+            val job = underTest.carrierConfigStream.onEach { latest = it }.launchIn(this)
+
+            sendConfig(SUB_ID_1)
+            assertThat(latest).isEqualTo(Pair(SUB_ID_1, CONFIG_1))
+
+            sendConfig(SUB_ID_2)
+            assertThat(latest).isEqualTo(Pair(SUB_ID_2, CONFIG_2))
+
+            job.cancel()
+        }
+
+    @Test
+    fun `carrier config stream ignores invalid subscriptions`() =
+        testScope.runTest {
+            var latest: Pair<Int, PersistableBundle>? = null
+            val job = underTest.carrierConfigStream.onEach { latest = it }.launchIn(this)
+
+            sendConfig(INVALID_SUBSCRIPTION_ID)
+
+            assertThat(latest).isNull()
+
+            job.cancel()
+        }
+
+    @Test
+    fun `getOrCreateConfig - uses default config if no override`() {
+        val config = underTest.getOrCreateConfigForSubId(123)
+        assertThat(config.isUsingDefault).isTrue()
+    }
+
+    @Test
+    fun `getOrCreateConfig - uses override if exists`() {
+        val config = underTest.getOrCreateConfigForSubId(SUB_ID_1)
+        assertThat(config.isUsingDefault).isFalse()
+    }
+
+    @Test
+    fun `config - updates while config stream is collected`() =
+        testScope.runTest {
+            CONFIG_1.putBoolean(CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false)
+
+            carrierConfigCoreStartable.start()
+
+            val config = underTest.getOrCreateConfigForSubId(SUB_ID_1)
+            assertThat(config.shouldInflateSignalStrength.value).isFalse()
+
+            CONFIG_1.putBoolean(CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true)
+            sendConfig(SUB_ID_1)
+
+            assertThat(config.shouldInflateSignalStrength.value).isTrue()
+        }
+
+    private fun sendConfig(subId: Int) {
+        fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+            receiver.onReceive(
+                context,
+                Intent(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)
+                    .putExtra(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, subId)
+            )
+        }
+    }
+
+    companion object {
+        private const val SUB_ID_1 = 1
+        private const val SUB_ID_2 = 2
+
+        private val DEFAULT_CONFIG = createTestConfig()
+        private val CONFIG_1 = createTestConfig()
+        private val CONFIG_2 = createTestConfig()
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
index 0add905e..cb9eb70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/FakeMobileConnectionsRepository.kt
@@ -71,9 +71,6 @@
             ?: FakeMobileConnectionRepository(subId, tableLogBuffer).also { subIdRepos[subId] = it }
     }
 
-    private val _globalMobileDataSettingChangedEvent = MutableStateFlow(Unit)
-    override val globalMobileDataSettingChangedEvent = _globalMobileDataSettingChangedEvent
-
     override val defaultDataSubRatConfig = MutableStateFlow(MobileMappings.Config())
 
     private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING)
@@ -94,10 +91,6 @@
         _mobileConnectivity.value = model
     }
 
-    suspend fun triggerGlobalMobileDataSettingChangedEvent() {
-        _globalMobileDataSettingChangedEvent.emit(Unit)
-    }
-
     fun setActiveMobileDataSubscriptionId(subId: Int) {
         _activeMobileDataSubscriptionId.value = subId
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
index 0859d14..4da2104 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/MobileRepositorySwitcherTest.kt
@@ -25,6 +25,7 @@
 import com.android.systemui.demomode.DemoMode
 import com.android.systemui.demomode.DemoModeController
 import com.android.systemui.dump.DumpManager
+import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.demo.DemoMobileConnectionsRepository
@@ -40,7 +41,6 @@
 import com.android.systemui.util.mockito.kotlinArgumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.FakeSettings
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
@@ -82,10 +82,10 @@
     @Mock private lateinit var subscriptionManager: SubscriptionManager
     @Mock private lateinit var telephonyManager: TelephonyManager
     @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var summaryLogger: TableLogBuffer
     @Mock private lateinit var demoModeController: DemoModeController
     @Mock private lateinit var dumpManager: DumpManager
 
-    private val globalSettings = FakeSettings()
     private val fakeNetworkEventsFlow = MutableStateFlow<FakeNetworkEventModel?>(null)
     private val mobileMappings = FakeMobileMappingsProxy()
 
@@ -116,9 +116,9 @@
                 subscriptionManager,
                 telephonyManager,
                 logger,
+                summaryLogger,
                 mobileMappings,
                 fakeBroadcastDispatcher,
-                globalSettings,
                 context,
                 IMMEDIATE,
                 scope,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
index 6989b514..00ce412 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionParameterizedTest.kt
@@ -138,7 +138,8 @@
                 assertThat(connectionInfo.carrierNetworkChangeActive)
                     .isEqualTo(model.carrierNetworkChange)
                 assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
-                assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
+                assertThat(conn.networkName.value)
+                    .isEqualTo(NetworkNameModel.IntentDerived(model.name))
 
                 // TODO(b/261029387): check these once we start handling them
                 assertThat(connectionInfo.isEmergencyOnly).isFalse()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
index f12d113..f60d92b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/demo/DemoMobileConnectionsRepositoryTest.kt
@@ -539,7 +539,8 @@
                 assertThat(connectionInfo.carrierNetworkChangeActive)
                     .isEqualTo(model.carrierNetworkChange)
                 assertThat(connectionInfo.isRoaming).isEqualTo(model.roaming)
-                assertThat(conn.networkName.value).isEqualTo(NetworkNameModel.Derived(model.name))
+                assertThat(conn.networkName.value)
+                    .isEqualTo(NetworkNameModel.IntentDerived(model.name))
 
                 // TODO(b/261029387) check these once we start handling them
                 assertThat(connectionInfo.isEmergencyOnly).isFalse()
@@ -594,9 +595,11 @@
     subId: Int = 1,
     level: Int = 1,
     numberOfLevels: Int = 4,
+    activity: Int = DATA_ACTIVITY_NONE,
 ): FakeWifiEventModel.CarrierMerged =
     FakeWifiEventModel.CarrierMerged(
         subscriptionId = subId,
         level = level,
         numberOfLevels = numberOfLevels,
+        activity = activity,
     )
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
index ea90150..abb4561 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/CarrierMergedConnectionRepositoryTest.kt
@@ -16,6 +16,7 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
+import android.telephony.TelephonyManager
 import android.testing.AndroidTestingRunner
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
@@ -27,6 +28,7 @@
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
 import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
+import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
@@ -49,6 +51,7 @@
 
     private lateinit var wifiRepository: FakeWifiRepository
     @Mock private lateinit var logger: TableLogBuffer
+    @Mock private lateinit var telephonyManager: TelephonyManager
 
     private val testDispatcher = UnconfinedTestDispatcher()
     private val testScope = TestScope(testDispatcher)
@@ -56,13 +59,16 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        whenever(telephonyManager.subscriptionId).thenReturn(SUB_ID)
+        whenever(telephonyManager.simOperatorName).thenReturn("")
+
         wifiRepository = FakeWifiRepository()
 
         underTest =
             CarrierMergedConnectionRepository(
                 SUB_ID,
                 logger,
-                NetworkNameModel.Default("name"),
+                telephonyManager,
                 testScope.backgroundScope,
                 wifiRepository,
             )
@@ -135,6 +141,44 @@
         }
 
     @Test
+    fun connectionInfo_activity_comesFromWifiActivity() =
+        testScope.runTest {
+            var latest: MobileConnectionModel? = null
+            val job = underTest.connectionInfo.onEach { latest = it }.launchIn(this)
+
+            wifiRepository.setIsWifiEnabled(true)
+            wifiRepository.setIsWifiDefault(true)
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID,
+                    level = 3,
+                )
+            )
+            wifiRepository.setWifiActivity(
+                DataActivityModel(
+                    hasActivityIn = true,
+                    hasActivityOut = false,
+                )
+            )
+
+            assertThat(latest!!.dataActivityDirection.hasActivityIn).isTrue()
+            assertThat(latest!!.dataActivityDirection.hasActivityOut).isFalse()
+
+            wifiRepository.setWifiActivity(
+                DataActivityModel(
+                    hasActivityIn = false,
+                    hasActivityOut = true,
+                )
+            )
+
+            assertThat(latest!!.dataActivityDirection.hasActivityIn).isFalse()
+            assertThat(latest!!.dataActivityDirection.hasActivityOut).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
     fun connectionInfo_carrierMergedWifi_wrongSubId_isDefault() =
         testScope.runTest {
             var latest: MobileConnectionModel? = null
@@ -244,6 +288,43 @@
             job.cancel()
         }
 
+    @Test
+    fun networkName_usesSimOperatorNameAsInitial() =
+        testScope.runTest {
+            whenever(telephonyManager.simOperatorName).thenReturn("Test SIM name")
+
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("Test SIM name"))
+
+            job.cancel()
+        }
+
+    @Test
+    fun networkName_updatesOnNetworkUpdate() =
+        testScope.runTest {
+            whenever(telephonyManager.simOperatorName).thenReturn("Test SIM name")
+
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("Test SIM name"))
+
+            whenever(telephonyManager.simOperatorName).thenReturn("New SIM name")
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId = NET_ID,
+                    subscriptionId = SUB_ID,
+                    level = 3,
+                )
+            )
+
+            assertThat(latest).isEqualTo(NetworkNameModel.SimDerived("New SIM name"))
+
+            job.cancel()
+        }
+
     private companion object {
         const val SUB_ID = 123
         const val NET_ID = 456
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
index c02a4df..c02ca01 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/FullMobileConnectionRepositoryTest.kt
@@ -16,24 +16,33 @@
 
 package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
+import android.telephony.ServiceState
+import android.telephony.SignalStrength
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyManager
 import androidx.test.filters.SmallTest
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.log.table.TableLogBufferFactory
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_EMERGENCY
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_OPERATOR
+import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectionModel.Companion.COL_PRIMARY_LEVEL
 import com.android.systemui.statusbar.pipeline.mobile.data.model.NetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository
-import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.MobileTelephonyHelpers.getTelephonyCallbackForType
+import com.android.systemui.statusbar.pipeline.wifi.data.model.WifiNetworkModel
+import com.android.systemui.statusbar.pipeline.wifi.data.repository.FakeWifiRepository
 import com.android.systemui.util.mockito.any
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
 import com.android.systemui.util.time.FakeSystemClock
 import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.test.TestScope
@@ -55,24 +64,18 @@
 class FullMobileConnectionRepositoryTest : SysuiTestCase() {
     private lateinit var underTest: FullMobileConnectionRepository
 
+    private val systemClock = FakeSystemClock()
     private val testDispatcher = UnconfinedTestDispatcher()
     private val testScope = TestScope(testDispatcher)
-    private val mobileMappings = FakeMobileMappingsProxy()
-    private val tableLogBuffer = mock<TableLogBuffer>()
+    private val tableLogBuffer = TableLogBuffer(maxSize = 100, name = "TestName", systemClock)
     private val mobileFactory = mock<MobileConnectionRepositoryImpl.Factory>()
     private val carrierMergedFactory = mock<CarrierMergedConnectionRepository.Factory>()
 
-    private lateinit var connectionsRepo: FakeMobileConnectionsRepository
-    private val globalMobileDataSettingChangedEvent: Flow<Unit>
-        get() = connectionsRepo.globalMobileDataSettingChangedEvent
-
     private lateinit var mobileRepo: FakeMobileConnectionRepository
     private lateinit var carrierMergedRepo: FakeMobileConnectionRepository
 
     @Before
     fun setUp() {
-        connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogBuffer)
-
         mobileRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
         carrierMergedRepo = FakeMobileConnectionRepository(SUB_ID, tableLogBuffer)
 
@@ -82,12 +85,10 @@
                     any(),
                     eq(DEFAULT_NAME),
                     eq(SEP),
-                    eq(globalMobileDataSettingChangedEvent),
                 )
             )
             .thenReturn(mobileRepo)
-        whenever(carrierMergedFactory.build(eq(SUB_ID), any(), eq(DEFAULT_NAME)))
-            .thenReturn(carrierMergedRepo)
+        whenever(carrierMergedFactory.build(eq(SUB_ID), any())).thenReturn(carrierMergedRepo)
     }
 
     @Test
@@ -109,7 +110,6 @@
                     tableLogBuffer,
                     DEFAULT_NAME,
                     SEP,
-                    globalMobileDataSettingChangedEvent
                 )
         }
 
@@ -126,7 +126,7 @@
 
             assertThat(underTest.activeRepo.value).isEqualTo(mobileRepo)
             assertThat(underTest.connectionInfo.value).isEqualTo(mobileConnectionInfo)
-            verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer, DEFAULT_NAME)
+            verify(carrierMergedFactory, never()).build(SUB_ID, tableLogBuffer)
         }
 
     @Test
@@ -310,7 +310,6 @@
                     startingIsCarrierMerged = false,
                     DEFAULT_NAME,
                     SEP,
-                    globalMobileDataSettingChangedEvent,
                 )
 
             val connection1Repeat =
@@ -319,7 +318,6 @@
                     startingIsCarrierMerged = false,
                     DEFAULT_NAME,
                     SEP,
-                    globalMobileDataSettingChangedEvent,
                 )
 
             assertThat(connection1.tableLogBuffer)
@@ -345,7 +343,6 @@
                     startingIsCarrierMerged = false,
                     DEFAULT_NAME,
                     SEP,
-                    globalMobileDataSettingChangedEvent,
                 )
 
             // WHEN a connection with the same sub ID but carrierMerged = true is created
@@ -355,7 +352,6 @@
                     startingIsCarrierMerged = true,
                     DEFAULT_NAME,
                     SEP,
-                    globalMobileDataSettingChangedEvent,
                 )
 
             // THEN the same table is re-used
@@ -363,8 +359,219 @@
                 .isSameInstanceAs(connection1Repeat.tableLogBuffer)
         }
 
-    // TODO(b/238425913): Verify that the logging switches correctly (once the carrier merged repo
-    //   implements logging).
+    @Test
+    fun connectionInfo_logging_notCarrierMerged_getsUpdates() =
+        testScope.runTest {
+            // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
+            val telephonyManager =
+                mock<TelephonyManager>().apply { whenever(this.simOperatorName).thenReturn("") }
+            createRealMobileRepo(telephonyManager)
+            createRealCarrierMergedRepo(telephonyManager, FakeWifiRepository())
+
+            initializeRepo(startingIsCarrierMerged = false)
+
+            val job = underTest.connectionInfo.launchIn(this)
+
+            // WHEN we set up some mobile connection info
+            val serviceState = ServiceState()
+            serviceState.setOperatorName("longName", "OpTypical", "1")
+            serviceState.isEmergencyOnly = false
+            getTelephonyCallbackForType<TelephonyCallback.ServiceStateListener>(telephonyManager)
+                .onServiceStateChanged(serviceState)
+
+            // THEN it's logged to the buffer
+            assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpTypical")
+            assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}false")
+
+            // WHEN we update mobile connection info
+            val serviceState2 = ServiceState()
+            serviceState2.setOperatorName("longName", "OpDiff", "1")
+            serviceState2.isEmergencyOnly = true
+            getTelephonyCallbackForType<TelephonyCallback.ServiceStateListener>(telephonyManager)
+                .onServiceStateChanged(serviceState2)
+
+            // THEN the updates are logged
+            assertThat(dumpBuffer()).contains("$COL_OPERATOR${BUFFER_SEPARATOR}OpDiff")
+            assertThat(dumpBuffer()).contains("$COL_EMERGENCY${BUFFER_SEPARATOR}true")
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_logging_carrierMerged_getsUpdates() =
+        testScope.runTest {
+            // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
+            val telephonyManager =
+                mock<TelephonyManager>().apply { whenever(this.simOperatorName).thenReturn("") }
+            createRealMobileRepo(telephonyManager)
+            val wifiRepository = FakeWifiRepository()
+            createRealCarrierMergedRepo(telephonyManager, wifiRepository)
+
+            initializeRepo(startingIsCarrierMerged = true)
+
+            val job = underTest.connectionInfo.launchIn(this)
+
+            // WHEN we set up carrier merged info
+            val networkId = 2
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId,
+                    SUB_ID,
+                    level = 3,
+                )
+            )
+
+            // THEN the carrier merged info is logged
+            assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
+
+            // WHEN we update the info
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId,
+                    SUB_ID,
+                    level = 1,
+                )
+            )
+
+            // THEN the updates are logged
+            assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_logging_updatesWhenCarrierMergedUpdates() =
+        testScope.runTest {
+            // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
+            val telephonyManager =
+                mock<TelephonyManager>().apply { whenever(this.simOperatorName).thenReturn("") }
+            createRealMobileRepo(telephonyManager)
+
+            val wifiRepository = FakeWifiRepository()
+            createRealCarrierMergedRepo(telephonyManager, wifiRepository)
+
+            initializeRepo(startingIsCarrierMerged = false)
+
+            val job = underTest.connectionInfo.launchIn(this)
+
+            // WHEN we set up some mobile connection info
+            val signalStrength = mock<SignalStrength>()
+            whenever(signalStrength.level).thenReturn(1)
+
+            getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
+                .onSignalStrengthsChanged(signalStrength)
+
+            // THEN it's logged to the buffer
+            assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
+
+            // WHEN isCarrierMerged is set to true
+            val networkId = 2
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId,
+                    SUB_ID,
+                    level = 3,
+                )
+            )
+            underTest.setIsCarrierMerged(true)
+
+            // THEN the carrier merged info is logged
+            assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
+
+            // WHEN the carrier merge network is updated
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId,
+                    SUB_ID,
+                    level = 4,
+                )
+            )
+
+            // THEN the new level is logged
+            assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4")
+
+            // WHEN isCarrierMerged is set to false
+            underTest.setIsCarrierMerged(false)
+
+            // THEN the typical info is logged
+            // Note: Since our first logs also had the typical info, we need to search the log
+            // contents for after our carrier merged level log.
+            val fullBuffer = dumpBuffer()
+            val carrierMergedContentIndex = fullBuffer.indexOf("${BUFFER_SEPARATOR}4")
+            val bufferAfterCarrierMerged = fullBuffer.substring(carrierMergedContentIndex)
+            assertThat(bufferAfterCarrierMerged).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}1")
+
+            // WHEN the normal network is updated
+            val newMobileInfo =
+                MobileConnectionModel(
+                    operatorAlphaShort = "Mobile Operator 2",
+                    primaryLevel = 0,
+                )
+            mobileRepo.setConnectionInfo(newMobileInfo)
+
+            // THEN the new level is logged
+            assertThat(dumpBuffer()).contains("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}0")
+
+            job.cancel()
+        }
+
+    @Test
+    fun connectionInfo_logging_doesNotLogUpdatesForNotActiveRepo() =
+        testScope.runTest {
+            // SETUP: Use real repositories to verify the diffing still works. (See b/267501739.)
+            val telephonyManager =
+                mock<TelephonyManager>().apply { whenever(this.simOperatorName).thenReturn("") }
+            createRealMobileRepo(telephonyManager)
+
+            val wifiRepository = FakeWifiRepository()
+            createRealCarrierMergedRepo(telephonyManager, wifiRepository)
+
+            // WHEN isCarrierMerged = false
+            initializeRepo(startingIsCarrierMerged = false)
+
+            val job = underTest.connectionInfo.launchIn(this)
+
+            val signalStrength = mock<SignalStrength>()
+            whenever(signalStrength.level).thenReturn(1)
+            getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
+                .onSignalStrengthsChanged(signalStrength)
+
+            // THEN updates to the carrier merged level aren't logged
+            val networkId = 2
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId,
+                    SUB_ID,
+                    level = 4,
+                )
+            )
+            assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}4")
+
+            wifiRepository.setWifiNetwork(
+                WifiNetworkModel.CarrierMerged(
+                    networkId,
+                    SUB_ID,
+                    level = 3,
+                )
+            )
+            assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}3")
+
+            // WHEN isCarrierMerged is set to true
+            underTest.setIsCarrierMerged(true)
+
+            // THEN updates to the normal level aren't logged
+            whenever(signalStrength.level).thenReturn(5)
+            getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
+                .onSignalStrengthsChanged(signalStrength)
+            assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}5")
+
+            whenever(signalStrength.level).thenReturn(6)
+            getTelephonyCallbackForType<TelephonyCallback.SignalStrengthsListener>(telephonyManager)
+                .onSignalStrengthsChanged(signalStrength)
+            assertThat(dumpBuffer()).doesNotContain("$COL_PRIMARY_LEVEL${BUFFER_SEPARATOR}6")
+
+            job.cancel()
+        }
 
     private fun initializeRepo(startingIsCarrierMerged: Boolean) {
         underTest =
@@ -374,16 +581,74 @@
                 tableLogBuffer,
                 DEFAULT_NAME,
                 SEP,
-                globalMobileDataSettingChangedEvent,
                 testScope.backgroundScope,
                 mobileFactory,
                 carrierMergedFactory,
             )
     }
 
+    private fun createRealMobileRepo(
+        telephonyManager: TelephonyManager,
+    ): MobileConnectionRepositoryImpl {
+        whenever(telephonyManager.subscriptionId).thenReturn(SUB_ID)
+
+        val realRepo =
+            MobileConnectionRepositoryImpl(
+                context,
+                SUB_ID,
+                defaultNetworkName = NetworkNameModel.Default("default"),
+                networkNameSeparator = SEP,
+                telephonyManager,
+                systemUiCarrierConfig = mock(),
+                fakeBroadcastDispatcher,
+                mobileMappingsProxy = mock(),
+                testDispatcher,
+                logger = mock(),
+                tableLogBuffer,
+                testScope.backgroundScope,
+            )
+        whenever(
+                mobileFactory.build(
+                    eq(SUB_ID),
+                    any(),
+                    eq(DEFAULT_NAME),
+                    eq(SEP),
+                )
+            )
+            .thenReturn(realRepo)
+
+        return realRepo
+    }
+
+    private fun createRealCarrierMergedRepo(
+        telephonyManager: TelephonyManager,
+        wifiRepository: FakeWifiRepository,
+    ): CarrierMergedConnectionRepository {
+        wifiRepository.setIsWifiEnabled(true)
+        wifiRepository.setIsWifiDefault(true)
+        val realRepo =
+            CarrierMergedConnectionRepository(
+                SUB_ID,
+                tableLogBuffer,
+                telephonyManager,
+                testScope.backgroundScope,
+                wifiRepository,
+            )
+        whenever(carrierMergedFactory.build(eq(SUB_ID), any())).thenReturn(realRepo)
+
+        return realRepo
+    }
+
+    private fun dumpBuffer(): String {
+        val outputWriter = StringWriter()
+        tableLogBuffer.dump(PrintWriter(outputWriter), arrayOf())
+        return outputWriter.toString()
+    }
+
     private companion object {
         const val SUB_ID = 42
         private val DEFAULT_NAME = NetworkNameModel.Default("default name")
         private const val SEP = "-"
+        private const val BUFFER_SEPARATOR = "|"
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
index 314e250..a294088a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionRepositoryTest.kt
@@ -17,15 +17,13 @@
 package com.android.systemui.statusbar.pipeline.mobile.data.repository.prod
 
 import android.content.Intent
-import android.os.UserHandle
-import android.provider.Settings
+import android.telephony.CarrierConfigManager.KEY_INFLATE_SIGNAL_STRENGTH_BOOL
 import android.telephony.CellSignalStrengthCdma
 import android.telephony.NetworkRegistrationInfo
 import android.telephony.ServiceState
 import android.telephony.ServiceState.STATE_IN_SERVICE
 import android.telephony.ServiceState.STATE_OUT_OF_SERVICE
 import android.telephony.SignalStrength
-import android.telephony.SubscriptionInfo
 import android.telephony.TelephonyCallback
 import android.telephony.TelephonyCallback.DataActivityListener
 import android.telephony.TelephonyCallback.ServiceStateListener
@@ -61,6 +59,9 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.DefaultNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.OverrideNetworkType
 import com.android.systemui.statusbar.pipeline.mobile.data.model.ResolvedNetworkType.UnknownNetworkType
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfig
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.configWithOverride
+import com.android.systemui.statusbar.pipeline.mobile.data.model.SystemUiCarrierConfigTest.Companion.createTestConfig
 import com.android.systemui.statusbar.pipeline.mobile.data.model.toNetworkNameModel
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository.Companion.DEFAULT_NUM_LEVELS
@@ -69,10 +70,8 @@
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
 import com.android.systemui.statusbar.pipeline.shared.data.model.toMobileDataActivityModel
 import com.android.systemui.util.mockito.any
-import com.android.systemui.util.mockito.argumentCaptor
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -85,7 +84,6 @@
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mock
-import org.mockito.Mockito
 import org.mockito.MockitoAnnotations
 
 @Suppress("EXPERIMENTAL_IS_NOT_ENABLED")
@@ -101,12 +99,15 @@
 
     private val scope = CoroutineScope(IMMEDIATE)
     private val mobileMappings = FakeMobileMappingsProxy()
-    private val globalSettings = FakeSettings()
+    private val systemUiCarrierConfig =
+        SystemUiCarrierConfig(
+            SUB_1_ID,
+            createTestConfig(),
+        )
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
-        globalSettings.userId = UserHandle.USER_ALL
         whenever(telephonyManager.subscriptionId).thenReturn(SUB_1_ID)
 
         connectionsRepo = FakeMobileConnectionsRepository(mobileMappings, tableLogger)
@@ -118,9 +119,8 @@
                 DEFAULT_NAME,
                 SEP,
                 telephonyManager,
-                globalSettings,
+                systemUiCarrierConfig,
                 fakeBroadcastDispatcher,
-                connectionsRepo.globalMobileDataSettingChangedEvent,
                 mobileMappings,
                 IMMEDIATE,
                 logger,
@@ -400,52 +400,26 @@
     @Test
     fun dataEnabled_initial_false() =
         runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
-
-            assertThat(underTest.dataEnabled.value).isFalse()
-        }
-
-    @Test
-    fun dataEnabled_isEnabled_true() =
-        runBlocking(IMMEDIATE) {
-            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
-            val job = underTest.dataEnabled.launchIn(this)
-
-            assertThat(underTest.dataEnabled.value).isTrue()
-
-            job.cancel()
-        }
-
-    @Test
-    fun dataEnabled_isDisabled() =
-        runBlocking(IMMEDIATE) {
             whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
-            val job = underTest.dataEnabled.launchIn(this)
 
             assertThat(underTest.dataEnabled.value).isFalse()
-
-            job.cancel()
         }
 
     @Test
-    fun isDataConnectionAllowed_subIdSettingUpdate_valueUpdated() =
+    fun `is data enabled - tracks telephony callback`() =
         runBlocking(IMMEDIATE) {
-            val subIdSettingName = "${Settings.Global.MOBILE_DATA}$SUB_1_ID"
-
             var latest: Boolean? = null
             val job = underTest.dataEnabled.onEach { latest = it }.launchIn(this)
 
-            // We don't read the setting directly, we query telephony when changes happen
             whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
-            globalSettings.putInt(subIdSettingName, 0)
-            assertThat(latest).isFalse()
+            assertThat(underTest.dataEnabled.value).isFalse()
 
-            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(true)
-            globalSettings.putInt(subIdSettingName, 1)
+            val callback = getTelephonyCallbackForType<TelephonyCallback.DataEnabledListener>()
+
+            callback.onDataEnabledChanged(true, 1)
             assertThat(latest).isTrue()
 
-            whenever(telephonyManager.isDataConnectionAllowed).thenReturn(false)
-            globalSettings.putInt(subIdSettingName, 0)
+            callback.onDataEnabledChanged(false, 1)
             assertThat(latest).isFalse()
 
             job.cancel()
@@ -466,8 +440,6 @@
     fun `roaming - cdma - queries telephony manager`() =
         runBlocking(IMMEDIATE) {
             var latest: Boolean? = null
-            // Start the telephony collection job so that cdmaRoaming starts updating
-            val telephonyJob = underTest.connectionInfo.launchIn(this)
             val job = underTest.cdmaRoaming.onEach { latest = it }.launchIn(this)
 
             val cb = getTelephonyCallbackForType<ServiceStateListener>()
@@ -487,7 +459,6 @@
 
             assertThat(latest).isTrue()
 
-            telephonyJob.cancel()
             job.cancel()
         }
 
@@ -584,16 +555,51 @@
         }
 
     @Test
-    fun `network name - broadcast not for this sub id - returns default`() =
+    fun `network name - broadcast not for this sub id - keeps old value`() =
         runBlocking(IMMEDIATE) {
             var latest: NetworkNameModel? = null
             val job = underTest.networkName.onEach { latest = it }.launchIn(this)
 
-            val intent = spnIntent(subId = 101)
-
+            val intent = spnIntent()
             fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
                 receiver.onReceive(context, intent)
             }
+            assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+
+            // WHEN an intent with a different subId is sent
+            val wrongSubIntent = spnIntent(subId = 101)
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(context, wrongSubIntent)
+            }
+
+            // THEN the previous intent's name is still used
+            assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+
+            job.cancel()
+        }
+
+    @Test
+    fun `network name - broadcast has no data - updates to default`() =
+        runBlocking(IMMEDIATE) {
+            var latest: NetworkNameModel? = null
+            val job = underTest.networkName.onEach { latest = it }.launchIn(this)
+
+            val intent = spnIntent()
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(context, intent)
+            }
+            assertThat(latest).isEqualTo(intent.toNetworkNameModel(SEP))
+
+            val intentWithoutInfo =
+                spnIntent(
+                    showSpn = false,
+                    showPlmn = false,
+                )
+
+            fakeBroadcastDispatcher.registeredReceivers.forEach { receiver ->
+                receiver.onReceive(context, intentWithoutInfo)
+            }
 
             assertThat(latest).isEqualTo(DEFAULT_NAME)
 
@@ -601,7 +607,7 @@
         }
 
     @Test
-    fun `network name - operatorAlphaShort - tracked`() =
+    fun `operatorAlphaShort - tracked`() =
         runBlocking(IMMEDIATE) {
             var latest: String? = null
 
@@ -673,16 +679,31 @@
             job.cancel()
         }
 
-    private fun getTelephonyCallbacks(): List<TelephonyCallback> {
-        val callbackCaptor = argumentCaptor<TelephonyCallback>()
-        Mockito.verify(telephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
-        return callbackCaptor.allValues
-    }
+    @Test
+    fun `number of levels - uses carrier config`() =
+        runBlocking(IMMEDIATE) {
+            var latest: Int? = null
+            val job = underTest.numberOfLevels.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS)
+
+            systemUiCarrierConfig.processNewCarrierConfig(
+                configWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, true)
+            )
+
+            assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS + 1)
+
+            systemUiCarrierConfig.processNewCarrierConfig(
+                configWithOverride(KEY_INFLATE_SIGNAL_STRENGTH_BOOL, false)
+            )
+
+            assertThat(latest).isEqualTo(DEFAULT_NUM_LEVELS)
+
+            job.cancel()
+        }
 
     private inline fun <reified T> getTelephonyCallbackForType(): T {
-        val cbs = getTelephonyCallbacks().filterIsInstance<T>()
-        assertThat(cbs.size).isEqualTo(1)
-        return cbs[0]
+        return MobileTelephonyHelpers.getTelephonyCallbackForType(telephonyManager)
     }
 
     /** Convenience constructor for SignalStrength */
@@ -716,8 +737,6 @@
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
         private const val SUB_1_ID = 1
-        private val SUB_1 =
-            mock<SubscriptionInfo>().also { whenever(it.subscriptionId).thenReturn(SUB_1_ID) }
 
         private val DEFAULT_NAME = NetworkNameModel.Default("default name")
         private const val SEP = "-"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index db8172a..76e2bc0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -23,7 +23,6 @@
 import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
 import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
 import android.os.ParcelUuid
-import android.provider.Settings
 import android.telephony.CarrierConfigManager
 import android.telephony.SubscriptionInfo
 import android.telephony.SubscriptionManager
@@ -39,6 +38,7 @@
 import com.android.systemui.log.table.TableLogBufferFactory
 import com.android.systemui.statusbar.pipeline.mobile.data.model.MobileConnectivityModel
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
+import com.android.systemui.statusbar.pipeline.mobile.data.repository.CarrierConfigRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.prod.FullMobileConnectionRepository.Factory.Companion.tableBufferLogName
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
@@ -49,7 +49,6 @@
 import com.android.systemui.util.mockito.eq
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
-import com.android.systemui.util.settings.FakeSettings
 import com.google.common.truth.Truth.assertThat
 import java.util.UUID
 import kotlinx.coroutines.CoroutineScope
@@ -80,20 +79,22 @@
     private lateinit var carrierMergedFactory: CarrierMergedConnectionRepository.Factory
     private lateinit var fullConnectionFactory: FullMobileConnectionRepository.Factory
     private lateinit var wifiRepository: FakeWifiRepository
+    private lateinit var carrierConfigRepository: CarrierConfigRepository
     @Mock private lateinit var connectivityManager: ConnectivityManager
     @Mock private lateinit var subscriptionManager: SubscriptionManager
     @Mock private lateinit var telephonyManager: TelephonyManager
     @Mock private lateinit var logger: ConnectivityPipelineLogger
+    @Mock private lateinit var summaryLogger: TableLogBuffer
     @Mock private lateinit var logBufferFactory: TableLogBufferFactory
 
     private val mobileMappings = FakeMobileMappingsProxy()
 
     private val scope = CoroutineScope(IMMEDIATE)
-    private val globalSettings = FakeSettings()
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        whenever(telephonyManager.simOperatorName).thenReturn("")
 
         // Set up so the individual connection repositories
         whenever(telephonyManager.createForSubscriptionId(anyInt())).thenAnswer { invocation ->
@@ -119,19 +120,29 @@
 
         wifiRepository = FakeWifiRepository()
 
+        carrierConfigRepository =
+            CarrierConfigRepository(
+                fakeBroadcastDispatcher,
+                mock(),
+                mock(),
+                logger,
+                scope,
+            )
+
         connectionFactory =
             MobileConnectionRepositoryImpl.Factory(
                 fakeBroadcastDispatcher,
                 context = context,
                 telephonyManager = telephonyManager,
                 bgDispatcher = IMMEDIATE,
-                globalSettings = globalSettings,
                 logger = logger,
                 mobileMappingsProxy = mobileMappings,
                 scope = scope,
+                carrierConfigRepository = carrierConfigRepository,
             )
         carrierMergedFactory =
             CarrierMergedConnectionRepository.Factory(
+                telephonyManager,
                 scope,
                 wifiRepository,
             )
@@ -149,9 +160,9 @@
                 subscriptionManager,
                 telephonyManager,
                 logger,
+                summaryLogger,
                 mobileMappings,
                 fakeBroadcastDispatcher,
-                globalSettings,
                 context,
                 IMMEDIATE,
                 scope,
@@ -544,24 +555,6 @@
         }
 
     @Test
-    fun globalMobileDataSettingsChangedEvent_producesOnSettingChange() =
-        runBlocking(IMMEDIATE) {
-            var produced = false
-            val job =
-                underTest.globalMobileDataSettingChangedEvent
-                    .onEach { produced = true }
-                    .launchIn(this)
-
-            assertThat(produced).isFalse()
-
-            globalSettings.putInt(Settings.Global.MOBILE_DATA, 0)
-
-            assertThat(produced).isTrue()
-
-            job.cancel()
-        }
-
-    @Test
     fun mobileConnectivity_isConnected_isNotValidated() =
         runBlocking(IMMEDIATE) {
             val caps = createCapabilities(connected = true, validated = false)
@@ -627,9 +620,9 @@
                     subscriptionManager,
                     telephonyManager,
                     logger,
+                    summaryLogger,
                     mobileMappings,
                     fakeBroadcastDispatcher,
-                    globalSettings,
                     context,
                     IMMEDIATE,
                     scope,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt
new file mode 100644
index 0000000..621f793
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileTelephonyHelpers.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.mobile.data.repository.prod
+
+import android.telephony.TelephonyCallback
+import android.telephony.TelephonyManager
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.argumentCaptor
+import com.google.common.truth.Truth.assertThat
+import org.mockito.Mockito.verify
+
+/** Helper methods for telephony-related callbacks for mobile tests. */
+object MobileTelephonyHelpers {
+    fun getTelephonyCallbacks(mockTelephonyManager: TelephonyManager): List<TelephonyCallback> {
+        val callbackCaptor = argumentCaptor<TelephonyCallback>()
+        verify(mockTelephonyManager).registerTelephonyCallback(any(), callbackCaptor.capture())
+        return callbackCaptor.allValues
+    }
+
+    inline fun <reified T> getTelephonyCallbackForType(mockTelephonyManager: TelephonyManager): T {
+        val cbs = getTelephonyCallbacks(mockTelephonyManager).filterIsInstance<T>()
+        assertThat(cbs.size).isEqualTo(1)
+        return cbs[0]
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
index 7aeaa48..b645e66 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt
@@ -45,7 +45,7 @@
     private val _iconGroup = MutableStateFlow<SignalIcon.MobileIconGroup>(TelephonyIcons.THREE_G)
     override val networkTypeIconGroup = _iconGroup
 
-    override val networkName = MutableStateFlow(NetworkNameModel.Derived("demo mode"))
+    override val networkName = MutableStateFlow(NetworkNameModel.IntentDerived("demo mode"))
 
     private val _isEmergencyOnly = MutableStateFlow(false)
     override val isEmergencyOnly = _isEmergencyOnly
@@ -71,6 +71,8 @@
     private val _numberOfLevels = MutableStateFlow(DEFAULT_NUM_LEVELS)
     override val numberOfLevels = _numberOfLevels
 
+    override val isForceHidden = MutableStateFlow(false)
+
     fun setIconGroup(group: SignalIcon.MobileIconGroup) {
         _iconGroup.value = group
     }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
index 172755c..2699316 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt
@@ -73,6 +73,8 @@
     private val _isUserSetup = MutableStateFlow(true)
     override val isUserSetup = _isUserSetup
 
+    override val isForceHidden = MutableStateFlow(false)
+
     /** Always returns a new fake interactor */
     override fun createMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor {
         return FakeMobileIconInteractor(tableLogBuffer)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
index c42aba5..fa072fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt
@@ -66,6 +66,7 @@
                 mobileIconsInteractor.defaultMobileIconGroup,
                 mobileIconsInteractor.defaultDataSubId,
                 mobileIconsInteractor.isDefaultConnectionFailed,
+                mobileIconsInteractor.isForceHidden,
                 connectionRepository,
             )
     }
@@ -530,7 +531,7 @@
             )
             yield()
 
-            assertThat(latest).isEqualTo(NetworkNameModel.Derived(testOperatorName))
+            assertThat(latest).isEqualTo(NetworkNameModel.IntentDerived(testOperatorName))
 
             // Default network name, operator name is null, uses the default
             connectionRepository.setConnectionInfo(MobileConnectionModel(operatorAlphaShort = null))
@@ -550,6 +551,21 @@
             job.cancel()
         }
 
+    @Test
+    fun isForceHidden_matchesParent() =
+        runBlocking(IMMEDIATE) {
+            var latest: Boolean? = null
+            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+
+            mobileIconsInteractor.isForceHidden.value = true
+            assertThat(latest).isTrue()
+
+            mobileIconsInteractor.isForceHidden.value = false
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
     companion object {
         private val IMMEDIATE = Dispatchers.Main.immediate
 
@@ -559,6 +575,6 @@
         private const val SUB_1_ID = 1
 
         private val DEFAULT_NAME = NetworkNameModel.Default("test default name")
-        private val DERIVED_NAME = NetworkNameModel.Derived("test derived name")
+        private val DERIVED_NAME = NetworkNameModel.IntentDerived("test derived name")
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
index bd24922..f8a9783 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractorTest.kt
@@ -27,6 +27,8 @@
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
 import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
+import com.android.systemui.statusbar.pipeline.shared.data.model.ConnectivitySlot
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.util.CarrierConfigTracker
 import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.mockito.whenever
@@ -50,6 +52,7 @@
 @SmallTest
 class MobileIconsInteractorTest : SysuiTestCase() {
     private lateinit var underTest: MobileIconsInteractor
+    private lateinit var connectivityRepository: FakeConnectivityRepository
     private lateinit var connectionsRepository: FakeMobileConnectionsRepository
     private val userSetupRepository = FakeUserSetupRepository()
     private val mobileMappingsProxy = FakeMobileMappingsProxy()
@@ -63,6 +66,8 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        connectivityRepository = FakeConnectivityRepository()
+
         connectionsRepository = FakeMobileConnectionsRepository(mobileMappingsProxy, tableLogBuffer)
         connectionsRepository.setMobileConnectionRepositoryMap(
             mapOf(
@@ -79,6 +84,8 @@
                 connectionsRepository,
                 carrierConfigTracker,
                 logger = mock(),
+                tableLogger = mock(),
+                connectivityRepository,
                 userSetupRepository,
                 testScope.backgroundScope,
             )
@@ -609,6 +616,32 @@
             job.cancel()
         }
 
+    @Test
+    fun isForceHidden_repoHasMobileHidden_true() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+
+            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.MOBILE))
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isForceHidden_repoDoesNotHaveMobileHidden_false() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.isForceHidden.onEach { latest = it }.launchIn(this)
+
+            connectivityRepository.setForceHiddenIcons(setOf(ConnectivitySlot.WIFI))
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
     companion object {
         private val tableLogBuffer =
             TableLogBuffer(8, "MobileIconsInteractorTest", FakeSystemClock())
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
index a2c1209..e68a397 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/view/ModernStatusBarMobileViewTest.kt
@@ -29,12 +29,14 @@
 import com.android.systemui.log.table.TableLogBuffer
 import com.android.systemui.statusbar.StatusBarIconView
 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
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModel
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.QsMobileIconViewModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -58,31 +60,37 @@
 
     @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
     @Mock private lateinit var tableLogBuffer: TableLogBuffer
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var constants: ConnectivityConstants
+    private lateinit var interactor: FakeMobileIconInteractor
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
+    private lateinit var airplaneModeInteractor: AirplaneModeInteractor
 
+    private lateinit var viewModelCommon: MobileIconViewModel
     private lateinit var viewModel: LocationBasedMobileViewModel
 
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        // This line was necessary to make the onDarkChanged and setStaticDrawableColor tests pass.
+        // But, it maybe *shouldn't* be necessary.
+        whenever(constants.hasDataCapabilities).thenReturn(true)
+
         testableLooper = TestableLooper.get(this)
 
-        val interactor = FakeMobileIconInteractor(tableLogBuffer)
-
-        val viewModelCommon =
-            MobileIconViewModel(
-                subscriptionId = 1,
-                interactor,
-                logger,
-                constants,
-                testScope.backgroundScope,
+        airplaneModeRepository = FakeAirplaneModeRepository()
+        airplaneModeInteractor =
+            AirplaneModeInteractor(
+                airplaneModeRepository,
+                FakeConnectivityRepository(),
             )
-        viewModel = QsMobileIconViewModel(viewModelCommon, statusBarPipelineFlags)
+
+        interactor = FakeMobileIconInteractor(tableLogBuffer)
+        createViewModel()
     }
 
     // Note: The following tests are more like integration tests, since they stand up a full
-    // [WifiViewModel] and test the interactions between the view, view-binder, and view-model.
+    // [MobileIconViewModel] and test the interactions between the view, view-binder, and
+    // view-model.
 
     @Test
     fun setVisibleState_icon_iconShownDotHidden() {
@@ -130,7 +138,25 @@
     }
 
     @Test
-    fun isIconVisible_alwaysTrue() {
+    fun isIconVisible_noData_outputsFalse() {
+        whenever(constants.hasDataCapabilities).thenReturn(false)
+        createViewModel()
+
+        val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
+
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        assertThat(view.isIconVisible).isFalse()
+
+        ViewUtils.detachView(view)
+    }
+
+    @Test
+    fun isIconVisible_hasData_outputsTrue() {
+        whenever(constants.hasDataCapabilities).thenReturn(true)
+        createViewModel()
+
         val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
 
         ViewUtils.attachView(view)
@@ -142,6 +168,34 @@
     }
 
     @Test
+    fun isIconVisible_notAirplaneMode_outputsTrue() {
+        airplaneModeRepository.setIsAirplaneMode(false)
+
+        val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
+
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        assertThat(view.isIconVisible).isTrue()
+
+        ViewUtils.detachView(view)
+    }
+
+    @Test
+    fun isIconVisible_airplaneMode_outputsTrue() {
+        airplaneModeRepository.setIsAirplaneMode(true)
+
+        val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
+
+        ViewUtils.attachView(view)
+        testableLooper.processAllMessages()
+
+        assertThat(view.isIconVisible).isFalse()
+
+        ViewUtils.detachView(view)
+    }
+
+    @Test
     fun onDarkChanged_iconHasNewColor() {
         whenever(statusBarPipelineFlags.useDebugColoring()).thenReturn(false)
         val view = ModernStatusBarMobileView.constructAndBind(context, SLOT_NAME, viewModel)
@@ -184,6 +238,18 @@
     private fun View.getDotView(): View {
         return this.requireViewById(R.id.status_bar_dot)
     }
+
+    private fun createViewModel() {
+        viewModelCommon =
+            MobileIconViewModel(
+                subscriptionId = 1,
+                interactor,
+                airplaneModeInteractor,
+                constants,
+                testScope.backgroundScope,
+            )
+        viewModel = QsMobileIconViewModel(viewModelCommon, statusBarPipelineFlags)
+    }
 }
 
 private const val SLOT_NAME = "TestSlotName"
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
index c960a06..f983030 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt
@@ -21,10 +21,13 @@
 import com.android.systemui.SysuiTestCase
 import com.android.systemui.log.table.TableLogBuffer
 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
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel
 import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconViewModelTest.Companion.defaultSignal
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.flow.launchIn
@@ -46,8 +49,8 @@
     private lateinit var qsIcon: QsMobileIconViewModel
     private lateinit var keyguardIcon: KeyguardMobileIconViewModel
     private lateinit var interactor: FakeMobileIconInteractor
+    private lateinit var airplaneModeInteractor: AirplaneModeInteractor
     @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var constants: ConnectivityConstants
     @Mock private lateinit var tableLogBuffer: TableLogBuffer
 
@@ -57,6 +60,11 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        airplaneModeInteractor =
+            AirplaneModeInteractor(
+                FakeAirplaneModeRepository(),
+                FakeConnectivityRepository(),
+            )
         interactor = FakeMobileIconInteractor(tableLogBuffer)
         interactor.apply {
             setLevel(1)
@@ -68,7 +76,13 @@
             isDataConnected.value = true
         }
         commonImpl =
-            MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope)
+            MobileIconViewModel(
+                SUB_1_ID,
+                interactor,
+                airplaneModeInteractor,
+                constants,
+                testScope.backgroundScope,
+            )
 
         homeIcon = HomeMobileIconViewModel(commonImpl, statusBarPipelineFlags)
         qsIcon = QsMobileIconViewModel(commonImpl, statusBarPipelineFlags)
@@ -78,14 +92,14 @@
     @Test
     fun `location based view models receive same icon id when common impl updates`() =
         testScope.runTest {
-            var latestHome: Int? = null
-            val homeJob = homeIcon.iconId.onEach { latestHome = it }.launchIn(this)
+            var latestHome: SignalIconModel? = null
+            val homeJob = homeIcon.icon.onEach { latestHome = it }.launchIn(this)
 
-            var latestQs: Int? = null
-            val qsJob = qsIcon.iconId.onEach { latestQs = it }.launchIn(this)
+            var latestQs: SignalIconModel? = null
+            val qsJob = qsIcon.icon.onEach { latestQs = it }.launchIn(this)
 
-            var latestKeyguard: Int? = null
-            val keyguardJob = keyguardIcon.iconId.onEach { latestKeyguard = it }.launchIn(this)
+            var latestKeyguard: SignalIconModel? = null
+            val keyguardJob = keyguardIcon.icon.onEach { latestKeyguard = it }.launchIn(this)
 
             var expected = defaultSignal(level = 1)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
index b91a4df..bec276a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt
@@ -19,16 +19,18 @@
 import androidx.test.filters.SmallTest
 import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH
 import com.android.settingslib.AccessibilityContentDescriptions.PHONE_SIGNAL_STRENGTH_NONE
-import com.android.settingslib.graph.SignalDrawable
 import com.android.settingslib.mobile.TelephonyIcons.THREE_G
 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.log.table.TableLogBuffer
+import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
+import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconInteractor
+import com.android.systemui.statusbar.pipeline.mobile.ui.model.SignalIconModel
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
-import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
 import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.util.mockito.whenever
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -49,7 +51,8 @@
 class MobileIconViewModelTest : SysuiTestCase() {
     private lateinit var underTest: MobileIconViewModel
     private lateinit var interactor: FakeMobileIconInteractor
-    @Mock private lateinit var logger: ConnectivityPipelineLogger
+    private lateinit var airplaneModeRepository: FakeAirplaneModeRepository
+    private lateinit var airplaneModeInteractor: AirplaneModeInteractor
     @Mock private lateinit var constants: ConnectivityConstants
     @Mock private lateinit var tableLogBuffer: TableLogBuffer
 
@@ -59,6 +62,15 @@
     @Before
     fun setUp() {
         MockitoAnnotations.initMocks(this)
+        whenever(constants.hasDataCapabilities).thenReturn(true)
+
+        airplaneModeRepository = FakeAirplaneModeRepository()
+        airplaneModeInteractor =
+            AirplaneModeInteractor(
+                airplaneModeRepository,
+                FakeConnectivityRepository(),
+            )
+
         interactor = FakeMobileIconInteractor(tableLogBuffer)
         interactor.apply {
             setLevel(1)
@@ -69,15 +81,94 @@
             setNumberOfLevels(4)
             isDataConnected.value = true
         }
-        underTest =
-            MobileIconViewModel(SUB_1_ID, interactor, logger, constants, testScope.backgroundScope)
+        createAndSetViewModel()
     }
 
     @Test
+    fun isVisible_notDataCapable_alwaysFalse() =
+        testScope.runTest {
+            // Create a new view model here so the constants are properly read
+            whenever(constants.hasDataCapabilities).thenReturn(false)
+            createAndSetViewModel()
+
+            var latest: Boolean? = null
+            val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isVisible_notAirplane_notForceHidden_true() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+            airplaneModeRepository.setIsAirplaneMode(false)
+            interactor.isForceHidden.value = false
+
+            assertThat(latest).isTrue()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isVisible_airplane_false() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+            airplaneModeRepository.setIsAirplaneMode(true)
+            interactor.isForceHidden.value = false
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isVisible_forceHidden_false() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+            airplaneModeRepository.setIsAirplaneMode(false)
+            interactor.isForceHidden.value = true
+
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
+    fun isVisible_respondsToUpdates() =
+        testScope.runTest {
+            var latest: Boolean? = null
+            val job = underTest.isVisible.onEach { latest = it }.launchIn(this)
+
+            airplaneModeRepository.setIsAirplaneMode(false)
+            interactor.isForceHidden.value = false
+
+            assertThat(latest).isTrue()
+
+            airplaneModeRepository.setIsAirplaneMode(true)
+            assertThat(latest).isFalse()
+
+            airplaneModeRepository.setIsAirplaneMode(false)
+            assertThat(latest).isTrue()
+
+            interactor.isForceHidden.value = true
+            assertThat(latest).isFalse()
+
+            job.cancel()
+        }
+
+    @Test
     fun iconId_correctLevel_notCutout() =
         testScope.runTest {
-            var latest: Int? = null
-            val job = underTest.iconId.onEach { latest = it }.launchIn(this)
+            var latest: SignalIconModel? = null
+            val job = underTest.icon.onEach { latest = it }.launchIn(this)
             val expected = defaultSignal()
 
             assertThat(latest).isEqualTo(expected)
@@ -90,8 +181,8 @@
         testScope.runTest {
             interactor.setIsDefaultDataEnabled(false)
 
-            var latest: Int? = null
-            val job = underTest.iconId.onEach { latest = it }.launchIn(this)
+            var latest: SignalIconModel? = null
+            val job = underTest.icon.onEach { latest = it }.launchIn(this)
             val expected = defaultSignal(level = 1, connected = false)
 
             assertThat(latest).isEqualTo(expected)
@@ -102,8 +193,8 @@
     @Test
     fun `icon - uses empty state - when not in service`() =
         testScope.runTest {
-            var latest: Int? = null
-            val job = underTest.iconId.onEach { latest = it }.launchIn(this)
+            var latest: SignalIconModel? = null
+            val job = underTest.icon.onEach { latest = it }.launchIn(this)
 
             interactor.isInService.value = false
 
@@ -364,14 +455,7 @@
         testScope.runTest {
             // Create a new view model here so the constants are properly read
             whenever(constants.shouldShowActivityConfig).thenReturn(false)
-            underTest =
-                MobileIconViewModel(
-                    SUB_1_ID,
-                    interactor,
-                    logger,
-                    constants,
-                    testScope.backgroundScope,
-                )
+            createAndSetViewModel()
 
             var inVisible: Boolean? = null
             val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
@@ -403,14 +487,7 @@
         testScope.runTest {
             // Create a new view model here so the constants are properly read
             whenever(constants.shouldShowActivityConfig).thenReturn(true)
-            underTest =
-                MobileIconViewModel(
-                    SUB_1_ID,
-                    interactor,
-                    logger,
-                    constants,
-                    testScope.backgroundScope,
-                )
+            createAndSetViewModel()
 
             var inVisible: Boolean? = null
             val inJob = underTest.activityInVisible.onEach { inVisible = it }.launchIn(this)
@@ -459,6 +536,16 @@
             containerJob.cancel()
         }
 
+    private fun createAndSetViewModel() {
+        underTest = MobileIconViewModel(
+            SUB_1_ID,
+            interactor,
+            airplaneModeInteractor,
+            constants,
+            testScope.backgroundScope,
+        )
+    }
+
     companion object {
         private const val SUB_1_ID = 1
 
@@ -466,10 +553,11 @@
         fun defaultSignal(
             level: Int = 1,
             connected: Boolean = true,
-        ): Int {
-            return SignalDrawable.getState(level, /* numLevels */ 4, !connected)
+        ): SignalIconModel {
+            return SignalIconModel(level, numberOfLevels = 4, showExclamationMark = !connected)
         }
 
-        fun emptySignal(): Int = SignalDrawable.getEmptyState(4)
+        fun emptySignal(): SignalIconModel =
+            SignalIconModel(level = 0, numberOfLevels = 4, showExclamationMark = true)
     }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
index 58b50c7..d9268a2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModelTest.kt
@@ -20,11 +20,14 @@
 import com.android.systemui.SysuiTestCase
 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
 import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
 import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor
 import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityConstants
 import com.android.systemui.statusbar.pipeline.shared.ConnectivityPipelineLogger
+import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository
 import com.android.systemui.util.mockito.mock
 import com.google.common.truth.Truth.assertThat
 import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -46,6 +49,7 @@
     private lateinit var underTest: MobileIconsViewModel
     private val interactor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
 
+    private lateinit var airplaneModeInteractor: AirplaneModeInteractor
     @Mock private lateinit var statusBarPipelineFlags: StatusBarPipelineFlags
     @Mock private lateinit var logger: ConnectivityPipelineLogger
     @Mock private lateinit var constants: ConnectivityConstants
@@ -57,6 +61,12 @@
     fun setUp() {
         MockitoAnnotations.initMocks(this)
 
+        airplaneModeInteractor =
+            AirplaneModeInteractor(
+                FakeAirplaneModeRepository(),
+                FakeConnectivityRepository(),
+            )
+
         val subscriptionIdsFlow =
             interactor.filteredSubscriptions
                 .map { subs -> subs.map { it.subscriptionId } }
@@ -66,6 +76,7 @@
             MobileIconsViewModel(
                 subscriptionIdsFlow,
                 interactor,
+                airplaneModeInteractor,
                 logger,
                 constants,
                 testScope.backgroundScope,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt
index 64a93cf..6557754 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/DeviceControlsControllerImplTest.kt
@@ -38,6 +38,7 @@
 import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.PREFS_CONTROLS_SEEDING_COMPLETED
 import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.QS_DEFAULT_POSITION
 import com.android.systemui.statusbar.policy.DeviceControlsControllerImpl.Companion.QS_PRIORITY_POSITION
+import com.android.systemui.util.mockito.mock
 import com.android.systemui.util.settings.SecureSettings
 
 import java.util.Optional
@@ -102,6 +103,8 @@
         `when`(controlsComponent.getControlsListingController())
                 .thenReturn(Optional.of(controlsListingController))
 
+        `when`(controlsComponent.isEnabled()).thenReturn(true)
+
         controller = DeviceControlsControllerImpl(
             mContext,
             controlsComponent,
@@ -168,4 +171,15 @@
         seedCallback.value.accept(SeedResponse(TEST_PKG, true))
         verify(callback).onControlsUpdate(QS_DEFAULT_POSITION)
     }
+
+    @Test
+    fun testControlsDisabledRemoveFromAutoTracker() {
+        `when`(controlsComponent.isEnabled()).thenReturn(false)
+        val callback: DeviceControlsController.Callback = mock()
+
+        controller.setCallback(callback)
+
+        verify(callback).removeControlsAutoTracker()
+        verify(callback, never()).onControlsUpdate(anyInt())
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt
index 756397a..74ed7fb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/surfaceeffects/ripple/RippleAnimationTest.kt
@@ -42,13 +42,35 @@
                 pixelDensity = 2f,
                 color = Color.RED,
                 opacity = 30,
-                shouldFillRipple = true,
+                baseRingFadeParams =
+                    RippleShader.FadeParams(
+                        fadeInStart = 0f,
+                        fadeInEnd = 0.3f,
+                        fadeOutStart = 0.5f,
+                        fadeOutEnd = 1f
+                    ),
+                sparkleRingFadeParams =
+                    RippleShader.FadeParams(
+                        fadeInStart = 0.1f,
+                        fadeInEnd = 0.2f,
+                        fadeOutStart = 0.7f,
+                        fadeOutEnd = 0.9f
+                    ),
+                centerFillFadeParams =
+                    RippleShader.FadeParams(
+                        fadeInStart = 0f,
+                        fadeInEnd = 0.1f,
+                        fadeOutStart = 0.2f,
+                        fadeOutEnd = 0.3f
+                    ),
                 sparkleStrength = 0.3f
             )
         val rippleAnimation = RippleAnimation(config)
 
         with(rippleAnimation.rippleShader) {
-            assertThat(rippleFill).isEqualTo(config.shouldFillRipple)
+            assertThat(baseRingFadeParams).isEqualTo(config.baseRingFadeParams)
+            assertThat(sparkleRingFadeParams).isEqualTo(config.sparkleRingFadeParams)
+            assertThat(centerFillFadeParams).isEqualTo(config.centerFillFadeParams)
             assertThat(pixelDensity).isEqualTo(config.pixelDensity)
             assertThat(color).isEqualTo(ColorUtils.setAlphaComponent(config.color, config.opacity))
             assertThat(sparkleStrength).isEqualTo(config.sparkleStrength)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
index f4226bc..3d75967 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/unfold/FoldAodAnimationControllerTest.kt
@@ -25,6 +25,8 @@
 import androidx.test.filters.SmallTest
 import com.android.internal.util.LatencyTracker
 import com.android.systemui.SysuiTestCase
+import com.android.systemui.flags.FakeFeatureFlags
+import com.android.systemui.flags.Flags.FACE_AUTH_REFACTOR
 import com.android.systemui.keyguard.WakefulnessLifecycle
 import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
@@ -105,8 +107,13 @@
             }
 
         keyguardRepository = FakeKeyguardRepository()
+        val featureFlags = FakeFeatureFlags().apply { set(FACE_AUTH_REFACTOR, true) }
         val keyguardInteractor =
-            KeyguardInteractor(repository = keyguardRepository, commandQueue = commandQueue)
+            KeyguardInteractor(
+                repository = keyguardRepository,
+                commandQueue = commandQueue,
+                featureFlags = featureFlags
+            )
 
         // Needs to be run on the main thread
         runBlocking(IMMEDIATE) {
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 9bb52be..8660d09 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
@@ -117,8 +117,11 @@
             SUPERVISED_USER_CREATION_APP_PACKAGE,
         )
 
-        featureFlags = FakeFeatureFlags()
-        featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+        featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+                set(Flags.FACE_AUTH_REFACTOR, true)
+            }
         userRepository = FakeUserRepository()
         keyguardRepository = FakeKeyguardRepository()
         telephonyRepository = FakeTelephonyRepository()
@@ -139,6 +142,7 @@
                     KeyguardInteractor(
                         repository = keyguardRepository,
                         commandQueue = commandQueue,
+                        featureFlags = featureFlags,
                     ),
                 manager = manager,
                 applicationScope = testScope.backgroundScope,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
index 9a4ca56..8a35cb0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/user/ui/viewmodel/StatusBarUserChipViewModelTest.kt
@@ -82,7 +82,6 @@
 
     private val userRepository = FakeUserRepository()
     private val keyguardRepository = FakeKeyguardRepository()
-    private val featureFlags = FakeFeatureFlags()
     private lateinit var guestUserInteractor: GuestUserInteractor
     private lateinit var refreshUsersScheduler: RefreshUsersScheduler
 
@@ -233,6 +232,11 @@
         }
 
     private fun viewModel(): StatusBarUserChipViewModel {
+        val featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+                set(Flags.FACE_AUTH_REFACTOR, true)
+            }
         return StatusBarUserChipViewModel(
             context = context,
             interactor =
@@ -244,9 +248,9 @@
                         KeyguardInteractor(
                             repository = keyguardRepository,
                             commandQueue = commandQueue,
+                            featureFlags = featureFlags,
                         ),
-                    featureFlags =
-                        FakeFeatureFlags().apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) },
+                    featureFlags = featureFlags,
                     manager = manager,
                     applicationScope = testScope.backgroundScope,
                     telephonyInteractor =
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 3d4bbdb..1337d1b 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
@@ -134,6 +134,11 @@
                 resetOrExitSessionReceiver = resetOrExitSessionReceiver,
             )
 
+        val featureFlags =
+            FakeFeatureFlags().apply {
+                set(Flags.FULL_SCREEN_USER_SWITCHER, false)
+                set(Flags.FACE_AUTH_REFACTOR, true)
+            }
         underTest =
             UserSwitcherViewModel.Factory(
                     userInteractor =
@@ -145,11 +150,9 @@
                                 KeyguardInteractor(
                                     repository = keyguardRepository,
                                     commandQueue = commandQueue,
+                                    featureFlags = featureFlags
                                 ),
-                            featureFlags =
-                                FakeFeatureFlags().apply {
-                                    set(Flags.FULL_SCREEN_USER_SWITCHER, false)
-                                },
+                            featureFlags = featureFlags,
                             manager = manager,
                             applicationScope = testScope.backgroundScope,
                             telephonyInteractor =
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionalCoreStartableTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionalCoreStartableTest.java
new file mode 100644
index 0000000..5ef62c1
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/condition/ConditionalCoreStartableTest.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.util.condition;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.testing.AndroidTestingRunner;
+
+import androidx.test.filters.SmallTest;
+
+import com.android.systemui.CoreStartable;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.shared.condition.Condition;
+import com.android.systemui.shared.condition.Monitor;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+@SmallTest
+@RunWith(AndroidTestingRunner.class)
+public class ConditionalCoreStartableTest extends SysuiTestCase {
+    public static class FakeConditionalCoreStartable extends ConditionalCoreStartable {
+        interface Callback {
+            void onStart();
+            void bootCompleted();
+        }
+
+        private final Callback mCallback;
+
+        public FakeConditionalCoreStartable(Monitor monitor, Set<Condition> conditions,
+                Callback callback) {
+            super(monitor, conditions);
+            mCallback = callback;
+        }
+
+        @Override
+        protected void onStart() {
+            mCallback.onStart();
+        }
+
+        @Override
+        protected void bootCompleted() {
+            mCallback.bootCompleted();
+        }
+    }
+
+
+    final Set<Condition> mConditions = new HashSet<>();
+
+    @Mock
+    Condition mCondition;
+
+    @Mock
+    Monitor mMonitor;
+
+    @Mock
+    FakeConditionalCoreStartable.Callback mCallback;
+
+    @Mock
+    Monitor.Subscription.Token mSubscriptionToken;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        mConditions.clear();
+    }
+
+    /**
+     * Verifies that {@link ConditionalCoreStartable#onStart()} is predicated on conditions being
+     * met.
+     */
+    @Test
+    public void testOnStartCallback() {
+        final CoreStartable coreStartable =
+                new FakeConditionalCoreStartable(mMonitor,
+                        new HashSet<>(Arrays.asList(mCondition)),
+                        mCallback);
+
+        when(mMonitor.addSubscription(any())).thenReturn(mSubscriptionToken);
+        coreStartable.start();
+
+        final ArgumentCaptor<Monitor.Subscription> subscriptionCaptor = ArgumentCaptor.forClass(
+                Monitor.Subscription.class);
+        verify(mMonitor).addSubscription(subscriptionCaptor.capture());
+
+        final Monitor.Subscription subscription = subscriptionCaptor.getValue();
+
+        assertThat(subscription.getConditions()).containsExactly(mCondition);
+
+        verify(mCallback, never()).onStart();
+
+        subscription.getCallback().onConditionsChanged(true);
+
+        verify(mCallback).onStart();
+        verify(mMonitor).removeSubscription(mSubscriptionToken);
+    }
+
+
+    /**
+     * Verifies that {@link ConditionalCoreStartable#bootCompleted()} ()} is predicated on
+     * conditions being met.
+     */
+    @Test
+    public void testBootCompleted() {
+        final CoreStartable coreStartable =
+                new FakeConditionalCoreStartable(mMonitor,
+                        new HashSet<>(Arrays.asList(mCondition)),
+                        mCallback);
+
+        when(mMonitor.addSubscription(any())).thenReturn(mSubscriptionToken);
+        coreStartable.onBootCompleted();
+
+        final ArgumentCaptor<Monitor.Subscription> subscriptionCaptor = ArgumentCaptor.forClass(
+                Monitor.Subscription.class);
+        verify(mMonitor).addSubscription(subscriptionCaptor.capture());
+
+        final Monitor.Subscription subscription = subscriptionCaptor.getValue();
+
+        assertThat(subscription.getConditions()).containsExactly(mCondition);
+
+        verify(mCallback, never()).bootCompleted();
+
+        subscription.getCallback().onConditionsChanged(true);
+
+        verify(mCallback).bootCompleted();
+        verify(mMonitor).removeSubscription(mSubscriptionToken);
+    }
+}
diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
index 8c2c964..677871f 100644
--- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
+++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java
@@ -357,7 +357,6 @@
         params.width = WindowManager.LayoutParams.MATCH_PARENT;
         params.accessibilityTitle = context.getString(R.string.autofill_save_accessibility_title);
         params.windowAnimations = R.style.AutofillSaveAnimation;
-        params.setTrustedOverlay();
 
         show();
     }
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 1428fa8..d78fe86 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -12764,8 +12764,10 @@
         // restored. This distinction is important for system-process packages that live in the
         // system user's process but backup/restore data for non-system users.
         // TODO (b/123688746): Handle all system-process packages with singleton check.
-        final int instantiatedUserId =
-                PLATFORM_PACKAGE_NAME.equals(packageName) ? UserHandle.USER_SYSTEM : targetUserId;
+        boolean useSystemUser = PLATFORM_PACKAGE_NAME.equals(packageName)
+                || getPackageManagerInternal().getSystemUiServiceComponent().getPackageName()
+                        .equals(packageName);
+        final int instantiatedUserId = useSystemUser ? UserHandle.USER_SYSTEM : targetUserId;
 
         IPackageManager pm = AppGlobals.getPackageManager();
         ApplicationInfo app = null;
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index b8e3a3a..9f15128 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -3831,8 +3831,15 @@
         // VOLUME_CHANGED_ACTION intent to see if the current device is the one being modified
         final int currDev = getDeviceForStream(vi.getStreamType());
 
+        final boolean skipping = (currDev == ada.getInternalType());
+
         AudioService.sVolumeLogger.log(new DeviceVolumeEvent(vi.getStreamType(), index, ada,
-                currDev, callingPackage));
+                currDev, callingPackage, skipping));
+
+        if (skipping) {
+            // setDeviceVolume was called on a device currently being used
+            return;
+        }
 
         // TODO handle unmuting of current audio device
         // if a stream is not muted but the VolumeInfo is for muting, set the volume index
diff --git a/services/core/java/com/android/server/audio/AudioServiceEvents.java b/services/core/java/com/android/server/audio/AudioServiceEvents.java
index 6cbe03e..36908dc 100644
--- a/services/core/java/com/android/server/audio/AudioServiceEvents.java
+++ b/services/core/java/com/android/server/audio/AudioServiceEvents.java
@@ -174,15 +174,17 @@
         final String mDeviceAddress;
         final String mCaller;
         final int mDeviceForStream;
+        final boolean mSkipped;
 
         DeviceVolumeEvent(int streamType, int index, @NonNull AudioDeviceAttributes device,
-                int deviceForStream, String callingPackage) {
+                int deviceForStream, String callingPackage, boolean skipped) {
             mStream = streamType;
             mVolIndex = index;
             mDeviceNativeType = "0x" + Integer.toHexString(device.getInternalType());
             mDeviceAddress = device.getAddress();
             mDeviceForStream = deviceForStream;
             mCaller = callingPackage;
+            mSkipped = skipped;
             // log metrics
             new MediaMetrics.Item(MediaMetrics.Name.AUDIO_VOLUME_EVENT)
                     .set(MediaMetrics.Property.EVENT, "setDeviceVolume")
@@ -197,14 +199,18 @@
 
         @Override
         public String eventToString() {
-            return new StringBuilder("setDeviceVolume(stream:")
+            final StringBuilder sb = new StringBuilder("setDeviceVolume(stream:")
                     .append(AudioSystem.streamToString(mStream))
                     .append(" index:").append(mVolIndex)
                     .append(" device:").append(mDeviceNativeType)
                     .append(" addr:").append(mDeviceAddress)
-                    .append(") from ").append(mCaller)
-                    .append(" currDevForStream:Ox").append(Integer.toHexString(mDeviceForStream))
-                    .toString();
+                    .append(") from ").append(mCaller);
+            if (mSkipped) {
+                sb.append(" skipped [device in use]");
+            } else {
+                sb.append(" currDevForStream:Ox").append(Integer.toHexString(mDeviceForStream));
+            }
+            return sb.toString();
         }
     }
 
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 2b7fbfb..0ee25e2 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -1322,6 +1322,23 @@
     }
 
     /**
+     * @return Default refresh rate while the device has high brightness mode enabled for HDR.
+     */
+    public int getDefaultRefreshRateInHbmHdr() {
+        return mContext.getResources().getInteger(
+            R.integer.config_defaultRefreshRateInHbmHdr);
+    }
+
+    /**
+     * @return Default refresh rate while the device has high brightness mode enabled because of
+     * high lux.
+     */
+    public int getDefaultRefreshRateInHbmSunlight() {
+        return mContext.getResources().getInteger(
+            R.integer.config_defaultRefreshRateInHbmSunlight);
+    }
+
+    /**
      * @return Default refresh rate in the higher blocking zone of the associated display
      */
     public int getDefaultHighBlockingZoneRefreshRate() {
diff --git a/services/core/java/com/android/server/display/DisplayModeDirector.java b/services/core/java/com/android/server/display/DisplayModeDirector.java
index 7c6b667..1bbdc20 100644
--- a/services/core/java/com/android/server/display/DisplayModeDirector.java
+++ b/services/core/java/com/android/server/display/DisplayModeDirector.java
@@ -121,6 +121,10 @@
     private final DeviceConfigInterface mDeviceConfig;
     private final DeviceConfigDisplaySettings mDeviceConfigDisplaySettings;
 
+    @GuardedBy("mLock")
+    @Nullable
+    private DisplayDeviceConfig mDefaultDisplayDeviceConfig;
+
     // A map from the display ID to the collection of votes and their priority. The latter takes
     // the form of another map from the priority to the vote itself so that each priority is
     // guaranteed to have exactly one vote, which is also easily and efficiently replaceable.
@@ -160,6 +164,7 @@
         mDeviceConfigDisplaySettings = new DeviceConfigDisplaySettings();
         mSettingsObserver = new SettingsObserver(context, handler);
         mBrightnessObserver = new BrightnessObserver(context, handler, injector);
+        mDefaultDisplayDeviceConfig = null;
         mUdfpsObserver = new UdfpsObserver();
         final BallotBox ballotBox = (displayId, priority, vote) -> {
             synchronized (mLock) {
@@ -529,11 +534,15 @@
      * @param displayDeviceConfig configurations relating to the underlying display device.
      */
     public void defaultDisplayDeviceUpdated(DisplayDeviceConfig displayDeviceConfig) {
-        mSettingsObserver.setRefreshRates(displayDeviceConfig,
-            /* attemptLoadingFromDeviceConfig= */ true);
-        mBrightnessObserver.updateBlockingZoneThresholds(displayDeviceConfig,
-            /* attemptLoadingFromDeviceConfig= */ true);
-        mBrightnessObserver.reloadLightSensor(displayDeviceConfig);
+        synchronized (mLock) {
+            mDefaultDisplayDeviceConfig = displayDeviceConfig;
+            mSettingsObserver.setRefreshRates(displayDeviceConfig,
+                /* attemptLoadingFromDeviceConfig= */ true);
+            mBrightnessObserver.updateBlockingZoneThresholds(displayDeviceConfig,
+                /* attemptLoadingFromDeviceConfig= */ true);
+            mBrightnessObserver.reloadLightSensor(displayDeviceConfig);
+            mHbmObserver.setupHdrRefreshRates(displayDeviceConfig);
+        }
     }
 
     /**
@@ -2534,7 +2543,7 @@
      * HBM that are associated with that display. Restrictions are retrieved from
      * DisplayManagerInternal but originate in the display-device-config file.
      */
-    public static class HbmObserver implements DisplayManager.DisplayListener {
+    public class HbmObserver implements DisplayManager.DisplayListener {
         private final BallotBox mBallotBox;
         private final Handler mHandler;
         private final SparseIntArray mHbmMode = new SparseIntArray();
@@ -2554,10 +2563,24 @@
             mDeviceConfigDisplaySettings = displaySettings;
         }
 
-        public void observe() {
-            mRefreshRateInHbmSunlight = mDeviceConfigDisplaySettings.getRefreshRateInHbmSunlight();
-            mRefreshRateInHbmHdr = mDeviceConfigDisplaySettings.getRefreshRateInHbmHdr();
+        /**
+         * Sets up the refresh rate to be used when HDR is enabled
+         */
+        public void setupHdrRefreshRates(DisplayDeviceConfig displayDeviceConfig) {
+            mRefreshRateInHbmHdr = mDeviceConfigDisplaySettings
+                .getRefreshRateInHbmHdr(displayDeviceConfig);
+            mRefreshRateInHbmSunlight = mDeviceConfigDisplaySettings
+                .getRefreshRateInHbmSunlight(displayDeviceConfig);
+        }
 
+        /**
+         * Sets up the HDR refresh rates, and starts observing for the changes in the display that
+         * might impact it
+         */
+        public void observe() {
+            synchronized (mLock) {
+                setupHdrRefreshRates(mDefaultDisplayDeviceConfig);
+            }
             mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
             mInjector.registerDisplayListener(this, mHandler,
                     DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS
@@ -2789,26 +2812,33 @@
                     -1);
         }
 
-        public int getRefreshRateInHbmSunlight() {
-            final int defaultRefreshRateInHbmSunlight =
-                    mContext.getResources().getInteger(
-                            R.integer.config_defaultRefreshRateInHbmSunlight);
-
-            final int refreshRate = mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                    DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_SUNLIGHT,
-                    defaultRefreshRateInHbmSunlight);
-
+        public int getRefreshRateInHbmHdr(DisplayDeviceConfig displayDeviceConfig) {
+            int refreshRate =
+                    (displayDeviceConfig == null) ? mContext.getResources().getInteger(
+                            R.integer.config_defaultRefreshRateInHbmHdr)
+                            : displayDeviceConfig.getDefaultRefreshRateInHbmHdr();
+            try {
+                refreshRate = mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                    DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_HDR,
+                    refreshRate);
+            } catch (NullPointerException e) {
+                // Do Nothing
+            }
             return refreshRate;
         }
 
-        public int getRefreshRateInHbmHdr() {
-            final int defaultRefreshRateInHbmHdr =
-                    mContext.getResources().getInteger(R.integer.config_defaultRefreshRateInHbmHdr);
-
-            final int refreshRate = mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
-                    DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_HDR,
-                    defaultRefreshRateInHbmHdr);
-
+        public int getRefreshRateInHbmSunlight(DisplayDeviceConfig displayDeviceConfig) {
+            int refreshRate =
+                    (displayDeviceConfig == null) ? mContext.getResources()
+                        .getInteger(R.integer.config_defaultRefreshRateInHbmSunlight)
+                        : displayDeviceConfig.getDefaultRefreshRateInHbmSunlight();
+            try {
+                refreshRate = mDeviceConfig.getInt(DeviceConfig.NAMESPACE_DISPLAY_MANAGER,
+                    DisplayManager.DeviceConfig.KEY_REFRESH_RATE_IN_HBM_SUNLIGHT,
+                    refreshRate);
+            } catch (NullPointerException e) {
+                // Do Nothing
+            }
             return refreshRate;
         }
 
@@ -2858,14 +2888,18 @@
                         0).sendToTarget();
             }
 
-            final int refreshRateInHbmSunlight = getRefreshRateInHbmSunlight();
-            mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED,
-                    refreshRateInHbmSunlight, 0)
+            synchronized (mLock) {
+                final int refreshRateInHbmSunlight =
+                        getRefreshRateInHbmSunlight(mDefaultDisplayDeviceConfig);
+                mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_SUNLIGHT_CHANGED,
+                        refreshRateInHbmSunlight, 0)
                     .sendToTarget();
 
-            final int refreshRateInHbmHdr = getRefreshRateInHbmHdr();
-            mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_HDR_CHANGED, refreshRateInHbmHdr, 0)
+                final int refreshRateInHbmHdr =
+                        getRefreshRateInHbmHdr(mDefaultDisplayDeviceConfig);
+                mHandler.obtainMessage(MSG_REFRESH_RATE_IN_HBM_HDR_CHANGED, refreshRateInHbmHdr, 0)
                     .sendToTarget();
+            }
         }
 
         private int[] getIntArrayProperty(String prop) {
diff --git a/services/core/java/com/android/server/dreams/DreamController.java b/services/core/java/com/android/server/dreams/DreamController.java
index f87a146..61f7928 100644
--- a/services/core/java/com/android/server/dreams/DreamController.java
+++ b/services/core/java/com/android/server/dreams/DreamController.java
@@ -244,8 +244,6 @@
                 }
 
                 mListener.onDreamStopped(dream.mToken);
-            } else if (dream.mCanDoze && !mCurrentDream.mCanDoze) {
-                mListener.stopDozing(dream.mToken);
             }
 
         } finally {
@@ -292,7 +290,6 @@
      */
     public interface Listener {
         void onDreamStopped(Binder token);
-        void stopDozing(Binder token);
     }
 
     private final class DreamRecord implements DeathRecipient, ServiceConnection {
diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java
index 148b80e..5b375d7 100644
--- a/services/core/java/com/android/server/dreams/DreamManagerService.java
+++ b/services/core/java/com/android/server/dreams/DreamManagerService.java
@@ -499,12 +499,7 @@
         }
 
         synchronized (mLock) {
-            if (mCurrentDream == null) {
-                return;
-            }
-
-            final boolean sameDream = mCurrentDream.token == token;
-            if ((sameDream && mCurrentDream.isDozing) || (!sameDream && !mCurrentDream.isDozing)) {
+            if (mCurrentDream != null && mCurrentDream.token == token && mCurrentDream.isDozing) {
                 mCurrentDream.isDozing = false;
                 mDozeWakeLock.release();
                 mPowerManagerInternal.setDozeOverrideFromDreamManager(
@@ -665,6 +660,10 @@
 
         Slog.i(TAG, "Entering dreamland.");
 
+        if (mCurrentDream != null && mCurrentDream.isDozing) {
+            stopDozingInternal(mCurrentDream.token);
+        }
+
         mCurrentDream = new DreamRecord(name, userId, isPreviewMode, canDoze);
 
         if (!mCurrentDream.name.equals(mAmbientDisplayComponent)) {
@@ -770,11 +769,6 @@
                 }
             }
         }
-
-        @Override
-        public void stopDozing(Binder token) {
-            stopDozingInternal(token);
-        }
     };
 
     private final ContentObserver mDozeEnabledObserver = new ContentObserver(null) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index f7b9d8b..8aca912 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -3906,6 +3906,10 @@
         }
     }
 
+    boolean isFinishing() {
+        return finishing;
+    }
+
     /**
      * This method is to only be called from the client via binder when the activity is destroyed
      * AND finished.
@@ -8289,7 +8293,13 @@
     }
 
     void recomputeConfiguration() {
-        onRequestedOverrideConfigurationChanged(getRequestedOverrideConfiguration());
+        // We check if the current activity is transparent. In that case we need to
+        // recomputeConfiguration of the first opaque activity beneath, to allow a
+        // proper computation of the new bounds.
+        if (!mLetterboxUiController.applyOnOpaqueActivityBelow(
+                ActivityRecord::recomputeConfiguration)) {
+            onRequestedOverrideConfigurationChanged(getRequestedOverrideConfiguration());
+        }
     }
 
     boolean isInTransition() {
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index e04900c..47bdba3 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -250,10 +250,7 @@
         }
         ActivityRecord topActivity = mDisplayContent.topRunningActivity(
                     /* considerKeyguardState= */ true);
-        if (topActivity == null
-                // Checking windowing mode on activity level because we don't want to
-                // show toast in case of activity embedding.
-                || topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
+        if (!isTreatmentEnabledForActivity(topActivity)) {
             return;
         }
         showToast(R.string.display_rotation_camera_compat_toast_after_rotation);
@@ -309,21 +306,28 @@
     }
 
     boolean isActivityEligibleForOrientationOverride(@NonNull ActivityRecord activity) {
-        return isTreatmentEnabledForDisplay() && isCameraActiveInFullscreen(activity);
+        return isTreatmentEnabledForDisplay()
+                && isCameraActive(activity, /* mustBeFullscreen */ true);
     }
 
+
     /**
      * Whether camera compat treatment is applicable for the given activity.
      *
      * <p>Conditions that need to be met:
      * <ul>
-     *     <li>{@link #isCameraActiveForPackage} is {@code true} for the activity.
+     *     <li>Camera is active for the package.
      *     <li>The activity is in fullscreen
      *     <li>The activity has fixed orientation but not "locked" or "nosensor" one.
      * </ul>
      */
     boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity) {
-        return activity != null && isCameraActiveInFullscreen(activity)
+        return isTreatmentEnabledForActivity(activity, /* mustBeFullscreen */ true);
+    }
+
+    private boolean isTreatmentEnabledForActivity(@Nullable ActivityRecord activity,
+            boolean mustBeFullscreen) {
+        return activity != null && isCameraActive(activity, mustBeFullscreen)
                 && activity.getRequestedConfigurationOrientation() != ORIENTATION_UNDEFINED
                 // "locked" and "nosensor" values are often used by camera apps that can't
                 // handle dynamic changes so we shouldn't force rotate them.
@@ -331,8 +335,10 @@
                 && activity.getOverrideOrientation() != SCREEN_ORIENTATION_LOCKED;
     }
 
-    private boolean isCameraActiveInFullscreen(@NonNull ActivityRecord activity) {
-        return !activity.inMultiWindowMode()
+    private boolean isCameraActive(@NonNull ActivityRecord activity, boolean mustBeFullscreen) {
+        // Checking windowing mode on activity level because we don't want to
+        // apply treatment in case of activity embedding.
+        return (!mustBeFullscreen || !activity.inMultiWindowMode())
                 && mCameraIdPackageBiMap.containsPackageName(activity.packageName)
                 && activity.mLetterboxUiController.shouldForceRotateForCameraCompat();
     }
@@ -385,7 +391,8 @@
         }
         // Checking that the whole app is in multi-window mode as we shouldn't show toast
         // for the activity embedding case.
-        if (topActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
+        if (topActivity.getTask().getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW
+                && isTreatmentEnabledForActivity(topActivity, /* mustBeFullscreen */ false)) {
             showToast(R.string.display_rotation_camera_compat_toast_in_split_screen);
         }
     }
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 9681789..d2e8ad1 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -81,6 +81,7 @@
 import static java.lang.Boolean.FALSE;
 import static java.lang.Boolean.TRUE;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.ActivityManager.TaskDescription;
 import android.content.pm.ActivityInfo.ScreenOrientation;
@@ -104,7 +105,10 @@
 import com.android.server.wm.LetterboxConfiguration.LetterboxBackgroundType;
 
 import java.io.PrintWriter;
+import java.util.Optional;
 import java.util.function.BooleanSupplier;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
 
 /** Controls behaviour of the letterbox UI for {@link mActivityRecord}. */
 // TODO(b/185262487): Improve test coverage of this class. Parts of it are tested in
@@ -114,6 +118,9 @@
 // TODO(b/263021211): Consider renaming to more generic CompatUIController.
 final class LetterboxUiController {
 
+    private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE =
+            activityRecord -> activityRecord.fillsParent() && !activityRecord.isFinishing();
+
     private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
 
     private static final float UNDEFINED_ASPECT_RATIO = 0f;
@@ -1390,7 +1397,8 @@
             return;
         }
         final ActivityRecord firstOpaqueActivityBeneath = mActivityRecord.getTask().getActivity(
-                ActivityRecord::fillsParent, mActivityRecord, false /* includeBoundary */,
+                FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE /* callback */,
+                mActivityRecord /* boundary */, false /* includeBoundary */,
                 true /* traverseTopToBottom */);
         if (firstOpaqueActivityBeneath == null) {
             // We skip letterboxing if the translucent activity doesn't have any opaque
@@ -1466,6 +1474,32 @@
         return mInheritedCompatDisplayInsets;
     }
 
+    /**
+     * In case of translucent activities, it consumes the {@link ActivityRecord} of the first opaque
+     * activity beneath using the given consumer and returns {@code true}.
+     */
+    boolean applyOnOpaqueActivityBelow(@NonNull Consumer<ActivityRecord> consumer) {
+        return findOpaqueNotFinishingActivityBelow()
+                .map(activityRecord -> {
+                    consumer.accept(activityRecord);
+                    return true;
+                }).orElse(false);
+    }
+
+    /**
+     * @return The first not finishing opaque activity beneath the current translucent activity
+     * if it exists and the strategy is enabled.
+     */
+    private Optional<ActivityRecord> findOpaqueNotFinishingActivityBelow() {
+        if (!hasInheritedLetterboxBehavior() || mActivityRecord.getTask() == null) {
+            return Optional.empty();
+        }
+        return Optional.ofNullable(mActivityRecord.getTask().getActivity(
+                FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE /* callback */,
+                mActivityRecord /* boundary */, false /* includeBoundary */,
+                true /* traverseTopToBottom */));
+    }
+
     private void inheritConfiguration(ActivityRecord firstOpaque) {
         // To avoid wrong behaviour, we're not forcing a specific aspet ratio to activities
         // which are not already providing one (e.g. permission dialogs) and presumably also
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 45dacbb..03b3ef0 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -5679,14 +5679,6 @@
                     && imeTarget.compareTo(this) <= 0;
             return inTokenWithAndAboveImeTarget;
         }
-
-        // The condition is for the system dialog not belonging to any Activity.
-        // (^FLAG_NOT_FOCUSABLE & FLAG_ALT_FOCUSABLE_IM) means the dialog is still focusable but
-        // should be placed above the IME window.
-        if ((mAttrs.flags & (FLAG_NOT_FOCUSABLE | FLAG_ALT_FOCUSABLE_IM))
-                == FLAG_ALT_FOCUSABLE_IM && isTrustedOverlay() && canAddInternalSystemWindow()) {
-            return true;
-        }
         return false;
     }
 
diff --git a/services/incremental/IncrementalService.cpp b/services/incremental/IncrementalService.cpp
index a49577b..1aeb0ca 100644
--- a/services/incremental/IncrementalService.cpp
+++ b/services/incremental/IncrementalService.cpp
@@ -2816,6 +2816,12 @@
 
 binder::Status IncrementalService::DataLoaderStub::onStatusChanged(MountId mountId, int newStatus) {
     if (!isValid()) {
+        if (newStatus == IDataLoaderStatusListener::DATA_LOADER_BOUND) {
+            // Async "bound" came to already destroyed stub.
+            // Unbind immediately to avoid invalid stub sitting around in DataLoaderManagerService.
+            mService.mDataLoaderManager->unbindFromDataLoader(mountId);
+            return binder::Status::ok();
+        }
         return binder::Status::
                 fromServiceSpecificError(-EINVAL, "onStatusChange came to invalid DataLoaderStub");
     }
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index 80de823..0f2176f 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -205,6 +205,7 @@
     private static final String TAG = AlarmManagerServiceTest.class.getSimpleName();
     private static final int SYSTEM_UI_UID = 12345;
     private static final int TEST_CALLING_USER = UserHandle.getUserId(TEST_CALLING_UID);
+    private static final int TEST_CALLING_UID_2 = TEST_CALLING_UID + 1;
 
     private long mAppStandbyWindow;
     private long mAllowWhileIdleWindow;
@@ -3375,10 +3376,40 @@
             final int type = ((i & 1) == 0) ? ELAPSED_REALTIME : ELAPSED_REALTIME_WAKEUP;
             setTestAlarm(type, mNowElapsedTest + i, getNewMockPendingIntent());
         }
+        for (int i = 0; i < 4; i++) {
+            final int type = ((i & 1) == 0) ? ELAPSED_REALTIME : ELAPSED_REALTIME_WAKEUP;
+            setTestAlarm(
+                    type,
+                    mNowElapsedTest + i,
+                    getNewMockPendingIntent(),
+                    0,
+                    FLAG_STANDALONE,
+                    TEST_CALLING_UID_2);
+        }
         mNowElapsedTest += 100;
         mTestTimer.expire();
 
-        verify(() -> MetricsHelper.pushAlarmBatchDelivered(10, 5));
+        final ArgumentCaptor<int[]> uidsCaptor = ArgumentCaptor.forClass(int[].class);
+        final ArgumentCaptor<int[]> alarmsPerUidCaptor = ArgumentCaptor.forClass(int[].class);
+        final ArgumentCaptor<int[]> wakeupAlarmsPerUidCaptor = ArgumentCaptor.forClass(int[].class);
+
+        verify(() -> MetricsHelper.pushAlarmBatchDelivered(
+                eq(14),
+                eq(7),
+                uidsCaptor.capture(),
+                alarmsPerUidCaptor.capture(),
+                wakeupAlarmsPerUidCaptor.capture()));
+        assertEquals(2, uidsCaptor.getValue().length);
+        assertEquals(2, alarmsPerUidCaptor.getValue().length);
+        assertEquals(2, wakeupAlarmsPerUidCaptor.getValue().length);
+        final int uid1Idx = uidsCaptor.getValue()[0] == TEST_CALLING_UID ? 0 : 1;
+        final int uid2Idx = 1 - uid1Idx;
+        assertEquals(TEST_CALLING_UID, uidsCaptor.getValue()[uid1Idx]);
+        assertEquals(TEST_CALLING_UID_2, uidsCaptor.getValue()[uid2Idx]);
+        assertEquals(10, alarmsPerUidCaptor.getValue()[uid1Idx]);
+        assertEquals(5, wakeupAlarmsPerUidCaptor.getValue()[uid1Idx]);
+        assertEquals(4, alarmsPerUidCaptor.getValue()[uid2Idx]);
+        assertEquals(2, wakeupAlarmsPerUidCaptor.getValue()[uid2Idx]);
     }
 
     @Test
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 77e5d1d..d1fbc1e 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -53,6 +53,8 @@
     private static final int DEFAULT_REFRESH_RATE = 120;
     private static final int DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE = 55;
     private static final int DEFAULT_LOW_BLOCKING_ZONE_REFRESH_RATE = 95;
+    private static final int DEFAULT_REFRESH_RATE_IN_HBM_HDR = 90;
+    private static final int DEFAULT_REFRESH_RATE_IN_HBM_SUNLIGHT = 100;
     private static final int[] LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{10, 30};
     private static final int[] LOW_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{1, 21};
     private static final int[] HIGH_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE = new int[]{160};
@@ -240,6 +242,10 @@
                 DEFAULT_HIGH_BLOCKING_ZONE_REFRESH_RATE);
         assertEquals(mDisplayDeviceConfig.getDefaultPeakRefreshRate(), DEFAULT_PEAK_REFRESH_RATE);
         assertEquals(mDisplayDeviceConfig.getDefaultRefreshRate(), DEFAULT_REFRESH_RATE);
+        assertEquals(mDisplayDeviceConfig.getDefaultRefreshRateInHbmSunlight(),
+                DEFAULT_REFRESH_RATE_IN_HBM_SUNLIGHT);
+        assertEquals(mDisplayDeviceConfig.getDefaultRefreshRateInHbmHdr(),
+                DEFAULT_REFRESH_RATE_IN_HBM_HDR);
         assertArrayEquals(mDisplayDeviceConfig.getLowDisplayBrightnessThresholds(),
                 LOW_BRIGHTNESS_THRESHOLD_OF_PEAK_REFRESH_RATE);
         assertArrayEquals(mDisplayDeviceConfig.getLowAmbientBrightnessThresholds(),
@@ -578,6 +584,12 @@
         when(mResources.getIntArray(
                 R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate))
                 .thenReturn(HIGH_AMBIENT_THRESHOLD_OF_PEAK_REFRESH_RATE);
+        when(mResources.getInteger(
+            R.integer.config_defaultRefreshRateInHbmHdr))
+            .thenReturn(DEFAULT_REFRESH_RATE_IN_HBM_HDR);
+        when(mResources.getInteger(
+            R.integer.config_defaultRefreshRateInHbmSunlight))
+            .thenReturn(DEFAULT_REFRESH_RATE_IN_HBM_SUNLIGHT);
 
         mDisplayDeviceConfig = DisplayDeviceConfig.create(mContext, true);
     }
diff --git a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
index 2cc6bd5..ff37564 100644
--- a/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
+++ b/services/tests/servicestests/src/com/android/server/display/DisplayModeDirectorTest.java
@@ -1873,6 +1873,10 @@
             .thenReturn(65);
         when(resources.getInteger(R.integer.config_defaultRefreshRateInZone))
             .thenReturn(85);
+        when(resources.getInteger(R.integer.config_defaultRefreshRateInHbmHdr))
+            .thenReturn(95);
+        when(resources.getInteger(R.integer.config_defaultRefreshRateInHbmSunlight))
+            .thenReturn(100);
         when(resources.getIntArray(R.array.config_brightnessThresholdsOfPeakRefreshRate))
             .thenReturn(new int[]{5});
         when(resources.getIntArray(R.array.config_ambientThresholdsOfPeakRefreshRate))
@@ -1883,8 +1887,21 @@
         when(
             resources.getIntArray(R.array.config_highAmbientBrightnessThresholdsOfFixedRefreshRate))
             .thenReturn(new int[]{7000});
+        when(resources.getInteger(
+            com.android.internal.R.integer.config_displayWhiteBalanceBrightnessFilterHorizon))
+            .thenReturn(3);
+        ArgumentCaptor<TypedValue> valueArgumentCaptor = ArgumentCaptor.forClass(TypedValue.class);
+        doAnswer((Answer<Void>) invocation -> {
+            valueArgumentCaptor.getValue().type = 4;
+            valueArgumentCaptor.getValue().data = 13;
+            return null;
+        }).when(resources).getValue(eq(com.android.internal.R.dimen
+                .config_displayWhiteBalanceBrightnessFilterIntercept),
+                valueArgumentCaptor.capture(), eq(true));
         DisplayModeDirector director =
                 createDirectorFromRefreshRateArray(new float[]{60.0f, 90.0f}, 0);
+        SensorManager sensorManager = createMockSensorManager(createLightSensor());
+        director.start(sensorManager);
         // We don't expect any interaction with DeviceConfig when the director is initialized
         // because we explicitly avoid doing this as this can lead to a latency spike in the
         // startup of DisplayManagerService
@@ -1894,6 +1911,8 @@
                 0.0);
         assertEquals(director.getBrightnessObserver().getRefreshRateInHighZone(), 65);
         assertEquals(director.getBrightnessObserver().getRefreshRateInLowZone(), 85);
+        assertEquals(director.getHbmObserver().getRefreshRateInHbmHdr(), 95);
+        assertEquals(director.getHbmObserver().getRefreshRateInHbmSunlight(), 100);
         assertArrayEquals(director.getBrightnessObserver().getHighDisplayBrightnessThreshold(),
                 new int[]{250});
         assertArrayEquals(director.getBrightnessObserver().getHighAmbientBrightnessThreshold(),
@@ -1903,6 +1922,7 @@
         assertArrayEquals(director.getBrightnessObserver().getLowAmbientBrightnessThreshold(),
                 new int[]{10});
 
+
         // Notify that the default display is updated, such that DisplayDeviceConfig has new values
         DisplayDeviceConfig displayDeviceConfig = mock(DisplayDeviceConfig.class);
         when(displayDeviceConfig.getDefaultLowBlockingZoneRefreshRate()).thenReturn(50);
@@ -1913,6 +1933,8 @@
         when(displayDeviceConfig.getLowAmbientBrightnessThresholds()).thenReturn(new int[]{30});
         when(displayDeviceConfig.getHighDisplayBrightnessThresholds()).thenReturn(new int[]{210});
         when(displayDeviceConfig.getHighAmbientBrightnessThresholds()).thenReturn(new int[]{2100});
+        when(displayDeviceConfig.getDefaultRefreshRateInHbmHdr()).thenReturn(65);
+        when(displayDeviceConfig.getDefaultRefreshRateInHbmSunlight()).thenReturn(75);
         director.defaultDisplayDeviceUpdated(displayDeviceConfig);
 
         assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0);
@@ -1928,6 +1950,8 @@
                 new int[]{25});
         assertArrayEquals(director.getBrightnessObserver().getLowAmbientBrightnessThreshold(),
                 new int[]{30});
+        assertEquals(director.getHbmObserver().getRefreshRateInHbmHdr(), 65);
+        assertEquals(director.getHbmObserver().getRefreshRateInHbmSunlight(), 75);
 
         // Notify that the default display is updated, such that DeviceConfig has new values
         FakeDeviceConfig config = mInjector.getDeviceConfig();
@@ -1938,7 +1962,8 @@
         config.setLowDisplayBrightnessThresholds(new int[]{10});
         config.setHighDisplayBrightnessThresholds(new int[]{255});
         config.setHighAmbientBrightnessThresholds(new int[]{8000});
-
+        config.setRefreshRateInHbmHdr(70);
+        config.setRefreshRateInHbmSunlight(80);
         director.defaultDisplayDeviceUpdated(displayDeviceConfig);
 
         assertEquals(director.getSettingsObserver().getDefaultRefreshRate(), 60, 0.0);
@@ -1954,6 +1979,8 @@
                 new int[]{10});
         assertArrayEquals(director.getBrightnessObserver().getLowAmbientBrightnessThreshold(),
                 new int[]{20});
+        assertEquals(director.getHbmObserver().getRefreshRateInHbmHdr(), 70);
+        assertEquals(director.getHbmObserver().getRefreshRateInHbmSunlight(), 80);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
index 4954e89..c2b3783 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationCompatPolicyTests.java
@@ -147,6 +147,20 @@
     }
 
     @Test
+    public void testOpenedCameraInSplitScreen_orientationNotFixed_doNotShowToast() {
+        configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
+        spyOn(mTask);
+        spyOn(mDisplayRotationCompatPolicy);
+        doReturn(WINDOWING_MODE_MULTI_WINDOW).when(mActivity).getWindowingMode();
+        doReturn(WINDOWING_MODE_MULTI_WINDOW).when(mTask).getWindowingMode();
+
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+        verify(mDisplayRotationCompatPolicy, never()).showToast(
+                R.string.display_rotation_camera_compat_toast_in_split_screen);
+    }
+
+    @Test
     public void testOnScreenRotationAnimationFinished_treatmentNotEnabled_doNotShowToast() {
         when(mLetterboxConfiguration.isCameraCompatTreatmentEnabled(
                     /* checkDeviceConfig */ anyBoolean()))
@@ -172,7 +186,7 @@
     @Test
     public void testOnScreenRotationAnimationFinished_notFullscreen_doNotShowToast() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
-        doReturn(WINDOWING_MODE_MULTI_WINDOW).when(mActivity).getWindowingMode();
+        doReturn(true).when(mActivity).inMultiWindowMode();
         spyOn(mDisplayRotationCompatPolicy);
 
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
@@ -184,6 +198,18 @@
     }
 
     @Test
+    public void testOnScreenRotationAnimationFinished_orientationNotFixed_doNotShowToast() {
+        configureActivity(SCREEN_ORIENTATION_UNSPECIFIED);
+        mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+        spyOn(mDisplayRotationCompatPolicy);
+
+        mDisplayRotationCompatPolicy.onScreenRotationAnimationFinished();
+
+        verify(mDisplayRotationCompatPolicy, never()).showToast(
+                R.string.display_rotation_camera_compat_toast_after_rotation);
+    }
+
+    @Test
     public void testOnScreenRotationAnimationFinished_showToast() {
         configureActivity(SCREEN_ORIENTATION_PORTRAIT);
         mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index fb5fda1..c426913 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -255,6 +255,51 @@
     }
 
     @Test
+    public void testCheckOpaqueIsLetterboxedWhenStrategyIsApplied() {
+        mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
+        setUpDisplaySizeWithApp(2000, 1000);
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        // Translucent Activity
+        final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setLaunchedFromUid(mActivity.getUid())
+                .build();
+        doReturn(false).when(translucentActivity).fillsParent();
+        spyOn(mActivity);
+        mTask.addChild(translucentActivity);
+        verify(mActivity).isFinishing();
+    }
+
+    @Test
+    public void testTranslucentActivitiesWhenUnfolding() {
+        mWm.mLetterboxConfiguration.setTranslucentLetterboxingOverrideEnabled(true);
+        setUpDisplaySizeWithApp(2800, 1400);
+        mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */);
+        mActivity.mWmService.mLetterboxConfiguration.setLetterboxHorizontalPositionMultiplier(
+                1.0f /*letterboxVerticalPositionMultiplier*/);
+        prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT);
+        // We launch a transparent activity
+        final ActivityRecord translucentActivity = new ActivityBuilder(mAtm)
+                .setLaunchedFromUid(mActivity.getUid())
+                .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT)
+                .build();
+        doReturn(false).when(translucentActivity).fillsParent();
+        mTask.addChild(translucentActivity);
+
+        mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+        spyOn(mActivity);
+
+        // Halffold
+        setFoldablePosture(translucentActivity, true /* isHalfFolded */, false /* isTabletop */);
+        verify(mActivity).recomputeConfiguration();
+        clearInvocations(mActivity);
+
+        // Unfold
+        setFoldablePosture(translucentActivity, false /* isHalfFolded */, false /* isTabletop */);
+        verify(mActivity).recomputeConfiguration();
+    }
+
+    @Test
     public void testRestartProcessIfVisible() {
         setUpDisplaySizeWithApp(1000, 2500);
         doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity);
@@ -3323,14 +3368,20 @@
 
     }
 
-    private void setFoldablePosture(boolean isHalfFolded, boolean isTabletop) {
-        final DisplayRotation r = mActivity.mDisplayContent.getDisplayRotation();
+    private void setFoldablePosture(ActivityRecord activity, boolean isHalfFolded,
+            boolean isTabletop) {
+        final DisplayRotation r = activity.mDisplayContent.getDisplayRotation();
         doReturn(isHalfFolded).when(r).isDisplaySeparatingHinge();
         doReturn(false).when(r).isDeviceInPosture(any(DeviceState.class), anyBoolean());
         if (isHalfFolded) {
-            doReturn(true).when(r).isDeviceInPosture(DeviceState.HALF_FOLDED, isTabletop);
+            doReturn(true).when(r)
+                    .isDeviceInPosture(DeviceState.HALF_FOLDED, isTabletop);
         }
-        mActivity.recomputeConfiguration();
+        activity.recomputeConfiguration();
+    }
+
+    private void setFoldablePosture(boolean isHalfFolded, boolean isTabletop) {
+        setFoldablePosture(mActivity, isHalfFolded, isTabletop);
     }
 
     @Test
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
index 514aec1..219f441 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java
@@ -42,7 +42,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
-import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_TOAST;
 
 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
@@ -975,19 +974,6 @@
         assertFalse(sameTokenWindow.needsRelativeLayeringToIme());
     }
 
-    @UseTestDisplay(addWindows = {W_ACTIVITY, W_INPUT_METHOD})
-    @Test
-    public void testNeedsRelativeLayeringToIme_systemDialog() {
-        WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
-                mDisplayContent,
-                "SystemDialog", true);
-        mDisplayContent.setImeLayeringTarget(mAppWindow);
-        mAppWindow.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
-        makeWindowVisible(mImeWindow);
-        systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
-        assertTrue(systemDialogWindow.needsRelativeLayeringToIme());
-    }
-
     @Test
     public void testSetFreezeInsetsState() {
         final WindowState app = createWindow(null, TYPE_APPLICATION, "app");
diff --git a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
index 7959d82..77fca45 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ZOrderingTests.java
@@ -22,7 +22,6 @@
 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
-import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL;
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
@@ -32,7 +31,6 @@
 import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
 import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
-import static android.view.WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL;
 import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL;
 import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
@@ -545,28 +543,4 @@
         assertZOrderGreaterThan(mTransaction, popupWindow.getSurfaceControl(),
                 mDisplayContent.getImeContainer().getSurfaceControl());
     }
-
-    @Test
-    public void testSystemDialogWindow_expectHigherThanIme_inMultiWindow() {
-        // Simulate the app window is in multi windowing mode and being IME target
-        mAppWindow.getConfiguration().windowConfiguration.setWindowingMode(
-                WINDOWING_MODE_MULTI_WINDOW);
-        mDisplayContent.setImeLayeringTarget(mAppWindow);
-        mDisplayContent.setImeInputTarget(mAppWindow);
-        makeWindowVisible(mImeWindow);
-
-        // Create a popupWindow
-        final WindowState systemDialogWindow = createWindow(null, TYPE_SECURE_SYSTEM_OVERLAY,
-                mDisplayContent, "SystemDialog", true);
-        systemDialogWindow.mAttrs.flags |= FLAG_ALT_FOCUSABLE_IM;
-        spyOn(systemDialogWindow);
-
-        mDisplayContent.assignChildLayers(mTransaction);
-
-        // Verify the surface layer of the popupWindow should higher than IME
-        verify(systemDialogWindow).needsRelativeLayeringToIme();
-        assertThat(systemDialogWindow.needsRelativeLayeringToIme()).isTrue();
-        assertZOrderGreaterThan(mTransaction, systemDialogWindow.getSurfaceControl(),
-                mDisplayContent.getImeContainer().getSurfaceControl());
-    }
 }
diff --git a/tests/testables/src/android/testing/TestableSettingsProvider.java b/tests/testables/src/android/testing/TestableSettingsProvider.java
index fd92c65..c6f18fd 100644
--- a/tests/testables/src/android/testing/TestableSettingsProvider.java
+++ b/tests/testables/src/android/testing/TestableSettingsProvider.java
@@ -49,14 +49,15 @@
     }
 
     void clearValuesAndCheck(Context context) {
-        int userId = UserHandle.myUserId();
-        mValues.put(key("global", MY_UNIQUE_KEY, userId), MY_UNIQUE_KEY);
-        mValues.put(key("secure", MY_UNIQUE_KEY, userId), MY_UNIQUE_KEY);
-        mValues.put(key("system", MY_UNIQUE_KEY, userId), MY_UNIQUE_KEY);
-
+        // Ensure we swapped over to use TestableSettingsProvider
         Settings.Global.clearProviderForTest();
         Settings.Secure.clearProviderForTest();
         Settings.System.clearProviderForTest();
+
+        // putString will eventually invoking the mocked call() method and update mValues
+        Settings.Global.putString(context.getContentResolver(), MY_UNIQUE_KEY, MY_UNIQUE_KEY);
+        Settings.Secure.putString(context.getContentResolver(), MY_UNIQUE_KEY, MY_UNIQUE_KEY);
+        Settings.System.putString(context.getContentResolver(), MY_UNIQUE_KEY, MY_UNIQUE_KEY);
         // Verify that if any test is using TestableContext, they all have the correct settings
         // provider.
         assertEquals("Incorrect settings provider, test using incorrect Context?", MY_UNIQUE_KEY,